Skip to content

Commit 9117f05

Browse files
authored
Merge pull request #26 from jymchng/fix/fix-mutable
Fix/fix mutable
2 parents 72c5860 + 0b45771 commit 9117f05

File tree

16 files changed

+1170
-510
lines changed

16 files changed

+1170
-510
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
name: Build Wheels
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
tags:
8+
- "v*"
9+
10+
jobs:
11+
build_sdist:
12+
name: "sdist"
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os: [ubuntu-latest]
18+
steps:
19+
- name: Check out repository
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
fetch-tags: true
24+
25+
- name: Set up python 3.12
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: "3.12"
29+
30+
- name: Install poetry
31+
uses: snok/install-poetry@v1
32+
with:
33+
version: 1.8.5
34+
35+
- name: Build sdist
36+
shell: bash
37+
run: |
38+
poetry self add "poetry-dynamic-versioning[plugin]"
39+
poetry build --format=sdist
40+
41+
- uses: actions/upload-artifact@v4
42+
with:
43+
name: wheels-sdist
44+
path: dist/*.tar.gz
45+
46+
build_wheels:
47+
name: "${{ matrix.os }} ${{ matrix.arch }} py${{ matrix.python-version }}"
48+
runs-on: ${{ matrix.os }}
49+
strategy:
50+
fail-fast: false
51+
matrix:
52+
os: [ubuntu-latest, windows-latest, macos-latest]
53+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
54+
arch: [x86_64, x86, arm64, aarch64, ppc64le, s390x]
55+
exclude:
56+
# Windows exclusions
57+
- os: windows-latest
58+
arch: aarch64
59+
- os: windows-latest
60+
arch: ppc64le
61+
- os: windows-latest
62+
arch: s390x
63+
- os: windows-latest
64+
python-version: "3.8"
65+
arch: arm64
66+
# macOS exclusions
67+
- os: macos-latest
68+
arch: x86
69+
- os: macos-latest
70+
arch: aarch64
71+
- os: macos-latest
72+
arch: ppc64le
73+
- os: macos-latest
74+
arch: s390x
75+
# Ubuntu exclusions
76+
- os: ubuntu-latest
77+
arch: arm64
78+
- os: ubuntu-latest
79+
arch: x86
80+
81+
steps:
82+
- name: Check out repository
83+
uses: actions/checkout@v4
84+
with:
85+
fetch-depth: 0
86+
fetch-tags: true
87+
88+
- name: Set up QEMU
89+
if: runner.os == 'Linux' && matrix.arch != 'x86_64'
90+
uses: docker/setup-qemu-action@v3
91+
with:
92+
platforms: all
93+
94+
- name: Set up python ${{ matrix.python-version }}
95+
uses: actions/setup-python@v5
96+
with:
97+
python-version: ${{ matrix.python-version }}
98+
99+
- name: Install poetry
100+
uses: snok/install-poetry@v1
101+
with:
102+
version: 1.8.5
103+
104+
- name: Add Poetry to path
105+
shell: bash
106+
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
107+
108+
- name: Install dependencies
109+
shell: bash
110+
run: |
111+
poetry self add "poetry-dynamic-versioning[plugin]"
112+
poetry install --only main
113+
114+
- name: Build wheel
115+
shell: bash
116+
env:
117+
CIBW_ARCHS: ${{ matrix.arch }}
118+
run: poetry build --format=wheel
119+
120+
- name: Setup clean test environment
121+
shell: bash
122+
run: |
123+
python -m venv venv
124+
if [ "${{ runner.os }}" = "Windows" ]; then
125+
source venv/Scripts/activate
126+
else
127+
source venv/bin/activate
128+
fi
129+
python -m pip install --upgrade pip
130+
python -m pip install pytest pandas
131+
python -m pip install dist/*.whl
132+
133+
- name: Run tests
134+
shell: bash
135+
run: |
136+
if [ "${{ runner.os }}" = "Windows" ]; then
137+
source venv/Scripts/activate
138+
else
139+
source venv/bin/activate
140+
fi
141+
python -m pytest tests/
142+
143+
- name: Upload wheel
144+
uses: actions/upload-artifact@v4
145+
with:
146+
name: wheels-${{ matrix.os }}-${{ matrix.arch }}-py${{ matrix.python-version }}
147+
path: dist/*.whl
148+
149+
upload_to_pypi:
150+
if: startsWith(github.ref, 'refs/tags/v')
151+
needs: ["build_sdist", "build_wheels"]
152+
runs-on: ubuntu-latest
153+
steps:
154+
- uses: actions/download-artifact@v4
155+
with:
156+
path: wheels
157+
pattern: wheels-*
158+
merge-multiple: true
159+
160+
- uses: pypa/gh-action-pypi-publish@release/v1
161+
with:
162+
password: ${{ secrets.PYPI_TOKEN }}
163+
packages_dir: wheels/
164+
skip_existing: true

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ jobs:
7777
- name: Install poetry
7878
uses: snok/install-poetry@v1
7979
with:
80+
version: 1.8.5
8081
virtualenvs-create: true
8182
virtualenvs-in-project: true
8283
installer-parallel: false # Currently there seems to be some race-condition in windows

Makefile

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ PYD_FILES := newtypemethod.*-*.pyd newtypeinit.*-*.pyd $(PROJECT_DIR)/$(EXTENSIO
2222
BUILD_DIR := build
2323
PYTEST_FLAGS := -s -vv
2424

25-
.PHONY: all clean build test test-all test-debug test-custom test-free test-slots test-init test-leak install lint format check venv-poetry clean-deps docker-build docker-run docker-clean docker-demo dist-contents
25+
.PHONY: all clean build test test-all test-debug test-custom test-free test-slots test-init test-leak install lint format check venv-poetry clean-deps docker-build docker-run docker-clean docker-demo dist-contents check-version
2626

2727
# Default target
2828
all: clean build test format check venv-poetry clean-deps
@@ -85,12 +85,13 @@ clean-deps:
8585
# Build extensions
8686
build: clean
8787
$(POETRY) build
88+
89+
update-docs-deps:
8890
poetry lock && poetry export -f requirements.txt --output requirements-docs.txt --with docs
8991

9092
# Build with debug printing enabled
9193
build-debug: clean
92-
export __PYNT_DEBUG__="true" && make build
93-
poetry lock && poetry export -f requirements.txt --output requirements-docs.txt --with docs
94+
export __PYNT_DEBUG__="true" && $(POETRY) build && unset __PYNT_DEBUG__
9495

9596
# Install dependencies
9697
install: build
@@ -102,7 +103,7 @@ test:
102103

103104
# Run all tests with debug build
104105
test-debug: build-debug
105-
$(PYTHON) -m pytest . $(PYTEST_FLAGS) && unset __PYNT_DEBUG__
106+
$(PYTHON) -m pytest . $(PYTEST_FLAGS)
106107

107108
# Run specific test suites
108109
test-custom:
@@ -154,6 +155,30 @@ install-test: install-dev-deps dev
154155
list-packaged: build
155156
tar -tf $(shell ls -1 dist/*.tar.gz | sort -V | tail -n 1)
156157

158+
# Version verification
159+
check-version:
160+
@echo "Checking version consistency..."
161+
@DIST_FILE=$$(ls dist/python_newtype-*.tar.gz | sort -V | tail -n1); \
162+
if [ ! -f "$$DIST_FILE" ]; then \
163+
echo "Error: No distribution package found in dist/"; \
164+
exit 1; \
165+
fi; \
166+
DIST_BASE=$$(basename "$$DIST_FILE" .tar.gz); \
167+
DIST_VERSION=$$(tar -xOf "$$DIST_FILE" "$$DIST_BASE/newtype/__init__.py" | grep "__version__" | cut -d'"' -f2); \
168+
GIT_VERSION=$$(git describe --tags --abbrev=0 | sed 's/^v//'); \
169+
if [ -z "$$DIST_VERSION" ] || [ -z "$$GIT_VERSION" ]; then \
170+
echo "Error: Could not extract version information"; \
171+
exit 1; \
172+
fi; \
173+
if [ "$$DIST_VERSION" != "$$GIT_VERSION" ]; then \
174+
echo "Version mismatch:"; \
175+
echo " Distribution version: $$DIST_VERSION"; \
176+
echo " Git tag version: $$GIT_VERSION"; \
177+
exit 1; \
178+
else \
179+
echo "Version consistency check passed (version: $$DIST_VERSION)"; \
180+
fi
181+
157182
# Help target
158183
help:
159184
@echo "Available targets:"

examples/bounded_wrapped_ints.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class GenericWrappedBoundedInt_WithNewType(NewType(int)): # never mind about th
1010

1111
__CONCRETE_BOUNDED_INTS__ = WeakValueDictionary()
1212

13-
def __new__(self, value: int):
14-
inst = super().__new__(self, value % self.MAX_VALUE)
13+
def __new__(cls, value: int):
14+
inst = super().__new__(cls, value % cls.MAX_VALUE)
1515
return inst
1616

1717
def __repr__(self) -> str:

examples/newtype_enums.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from enum import Enum
2+
3+
import pytest
4+
5+
from newtype import NewType, newtype_exclude
6+
7+
8+
class ENV(NewType(str), Enum):
9+
10+
LOCAL = "LOCAL"
11+
DEV = "DEV"
12+
SIT = "SIT"
13+
UAT = "UAT"
14+
PREPROD = "PREPROD"
15+
PROD = "PROD"
16+
17+
class RegularENV(str, Enum):
18+
19+
LOCAL = "LOCAL"
20+
DEV = "DEV"
21+
SIT = "SIT"
22+
UAT = "UAT"
23+
PREPROD = "PREPROD"
24+
PROD = "PROD"
25+
26+
class ENVVariant(str):
27+
28+
__VALID_MEMBERS__ = ["LOCAL", "DEV", "SIT", "UAT", "PREPROD", "PROD"]
29+
30+
def __new__(cls, value: str):
31+
members = ENVVariant.__VALID_MEMBERS__
32+
# if isinstance(value, RollYourOwnNewTypeEnum):
33+
# value_as_str = str(value.value)
34+
# else:
35+
value_as_str = str(value)
36+
if value_as_str not in members:
37+
raise ValueError(f"`value` = {value} must be one of `{members}`; `value_as_str` = {value_as_str}")
38+
return super().__new__(cls, value_as_str)
39+
40+
# why not i write my own `.replace(..)`
41+
# yes, you can but how?
42+
def my_replace(self, old: "ENVVariant", new: "ENVVariant", count: int=-1):
43+
return ENVVariant(str(self).replace(str(old), str(new), count))
44+
45+
class RollYourOwnNewTypeEnum(ENVVariant, Enum):
46+
47+
LOCAL = "LOCAL"
48+
DEV = "DEV"
49+
SIT = "SIT"
50+
UAT = "UAT"
51+
PREPROD = "PREPROD"
52+
PROD = "PROD"
53+
54+
55+
def test_nt_env_replace():
56+
57+
env = ENV.LOCAL
58+
59+
assert env is ENV.LOCAL
60+
assert env is not ENV.DEV
61+
assert isinstance(env, ENV)
62+
63+
# let's say now we want to replace the environment
64+
# nevermind about the reason why we want to do so
65+
env = env.replace(ENV.LOCAL, ENV.DEV)
66+
67+
# replacement is successful
68+
assert env is ENV.DEV
69+
assert env is not ENV.LOCAL
70+
71+
# still an `ENV`
72+
assert isinstance(env, ENV)
73+
assert isinstance(env, str)
74+
75+
with pytest.raises(ValueError):
76+
# cannot replace with something that is not a `ENV`
77+
env = env.replace(ENV.DEV, "NotAnEnv")
78+
79+
with pytest.raises(ValueError):
80+
# cannot even make 'DEV' -> 'dev'
81+
env = env.lower()
82+
83+
def test_reg_env_replace():
84+
85+
env = RegularENV.LOCAL
86+
87+
# expected outcomes
88+
assert env is RegularENV.LOCAL # pass
89+
assert env is not RegularENV.DEV # pass
90+
assert isinstance(env, RegularENV) # pass
91+
92+
# now we try to replace
93+
env = env.replace(RegularENV.LOCAL, RegularENV.DEV)
94+
95+
# we are hoping that it will continue to be a `RegularENV.DEV` but it is not
96+
assert env is not RegularENV.DEV # pass, no longer a `RegularENV`
97+
assert env is not RegularENV.LOCAL # pass, no longer a `RegularENV`
98+
assert not isinstance(env, RegularENV)
99+
assert isinstance(env, str) # 'downcast' (?) to `str`
100+
101+
def test_ryont_env_replace():
102+
103+
env = RollYourOwnNewTypeEnum.LOCAL
104+
105+
# expected outcomes
106+
assert env is RollYourOwnNewTypeEnum.LOCAL # pass
107+
assert env is not RollYourOwnNewTypeEnum.DEV # pass
108+
assert isinstance(env, RollYourOwnNewTypeEnum) # pass
109+
110+
# now we try to replace
111+
env = env.replace(RollYourOwnNewTypeEnum.LOCAL, RollYourOwnNewTypeEnum.DEV)
112+
113+
# we are hoping that it will continue to be a `RollYourOwnNewTypeEnum.DEV` but it is not
114+
assert env is not RollYourOwnNewTypeEnum.DEV # pass, no longer a `RollYourOwnNewTypeEnum`
115+
assert env is not RollYourOwnNewTypeEnum.LOCAL # pass, no longer a `RollYourOwnNewTypeEnum`
116+
assert not isinstance(env, RollYourOwnNewTypeEnum)
117+
assert isinstance(env, str) # 'downcast' (?) to `str`
118+
119+
with pytest.raises(AssertionError):
120+
assert env is RollYourOwnNewTypeEnum.DEV
121+
122+
with pytest.raises(AssertionError):
123+
assert env is RollYourOwnNewTypeEnum.DEV
124+
125+
with pytest.raises(AssertionError):
126+
assert isinstance(env, RollYourOwnNewTypeEnum)
127+
128+
env = env.replace("DEV", "NotAnEnv")
129+
assert env == "NotAnEnv" # this 'shouldn't' pass but it does
130+
131+
env = RollYourOwnNewTypeEnum.LOCAL
132+
133+
# env = env.my_replace(RollYourOwnNewTypeEnum.LOCAL, RollYourOwnNewTypeEnum.PREPROD)
134+
135+
assert isinstance(env, str)
136+
assert env is not RollYourOwnNewTypeEnum.PREPROD
137+
assert isinstance(env, RollYourOwnNewTypeEnum)

0 commit comments

Comments
 (0)