Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .github/workflows/build_wheels.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: Build Wheels

on:
workflow_dispatch:
pull_request:
push:
tags:
- "v*"

jobs:
build_sdist:
name: "sdist"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Set up python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 1.8.5

- name: Build sdist
shell: bash
run: |
poetry self add "poetry-dynamic-versioning[plugin]"
poetry build --format=sdist

- uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist/*.tar.gz

build_wheels:
name: "${{ matrix.os }} ${{ matrix.arch }} py${{ matrix.python-version }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
arch: [x86_64, x86, arm64, aarch64, ppc64le, s390x]
exclude:
# Windows exclusions
- os: windows-latest
arch: aarch64
- os: windows-latest
arch: ppc64le
- os: windows-latest
arch: s390x
- os: windows-latest
python-version: "3.8"
arch: arm64
# macOS exclusions
- os: macos-latest
arch: x86
- os: macos-latest
arch: aarch64
- os: macos-latest
arch: ppc64le
- os: macos-latest
arch: s390x
# Ubuntu exclusions
- os: ubuntu-latest
arch: arm64
- os: ubuntu-latest
arch: x86

steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Set up QEMU
if: runner.os == 'Linux' && matrix.arch != 'x86_64'
uses: docker/setup-qemu-action@v3
with:
platforms: all

- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 1.8.5

- name: Add Poetry to path
shell: bash
run: echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Install dependencies
shell: bash
run: |
poetry self add "poetry-dynamic-versioning[plugin]"
poetry install --only main

- name: Build wheel
shell: bash
env:
CIBW_ARCHS: ${{ matrix.arch }}
run: poetry build --format=wheel

- name: Setup clean test environment
shell: bash
run: |
python -m venv venv
if [ "${{ runner.os }}" = "Windows" ]; then
source venv/Scripts/activate
else
source venv/bin/activate
fi
python -m pip install --upgrade pip
python -m pip install pytest pandas
python -m pip install dist/*.whl

- name: Run tests
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
source venv/Scripts/activate
else
source venv/bin/activate
fi
python -m pytest tests/

- name: Upload wheel
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ matrix.arch }}-py${{ matrix.python-version }}
path: dist/*.whl

upload_to_pypi:
if: startsWith(github.ref, 'refs/tags/v')
needs: ["build_sdist", "build_wheels"]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: wheels
pattern: wheels-*
merge-multiple: true

- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
packages_dir: wheels/
skip_existing: true
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 1.8.5
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: false # Currently there seems to be some race-condition in windows
Expand Down
33 changes: 29 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ PYD_FILES := newtypemethod.*-*.pyd newtypeinit.*-*.pyd $(PROJECT_DIR)/$(EXTENSIO
BUILD_DIR := build
PYTEST_FLAGS := -s -vv

.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
.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

# Default target
all: clean build test format check venv-poetry clean-deps
Expand Down Expand Up @@ -85,12 +85,13 @@ clean-deps:
# Build extensions
build: clean
$(POETRY) build

update-docs-deps:
poetry lock && poetry export -f requirements.txt --output requirements-docs.txt --with docs

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

# Install dependencies
install: build
Expand All @@ -102,7 +103,7 @@ test:

# Run all tests with debug build
test-debug: build-debug
$(PYTHON) -m pytest . $(PYTEST_FLAGS) && unset __PYNT_DEBUG__
$(PYTHON) -m pytest . $(PYTEST_FLAGS)

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

# Version verification
check-version:
@echo "Checking version consistency..."
@DIST_FILE=$$(ls dist/python_newtype-*.tar.gz | sort -V | tail -n1); \
if [ ! -f "$$DIST_FILE" ]; then \
echo "Error: No distribution package found in dist/"; \
exit 1; \
fi; \
DIST_BASE=$$(basename "$$DIST_FILE" .tar.gz); \
DIST_VERSION=$$(tar -xOf "$$DIST_FILE" "$$DIST_BASE/newtype/__init__.py" | grep "__version__" | cut -d'"' -f2); \
GIT_VERSION=$$(git describe --tags --abbrev=0 | sed 's/^v//'); \
if [ -z "$$DIST_VERSION" ] || [ -z "$$GIT_VERSION" ]; then \
echo "Error: Could not extract version information"; \
exit 1; \
fi; \
if [ "$$DIST_VERSION" != "$$GIT_VERSION" ]; then \
echo "Version mismatch:"; \
echo " Distribution version: $$DIST_VERSION"; \
echo " Git tag version: $$GIT_VERSION"; \
exit 1; \
else \
echo "Version consistency check passed (version: $$DIST_VERSION)"; \
fi

# Help target
help:
@echo "Available targets:"
Expand Down
4 changes: 2 additions & 2 deletions examples/bounded_wrapped_ints.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class GenericWrappedBoundedInt_WithNewType(NewType(int)): # never mind about th

__CONCRETE_BOUNDED_INTS__ = WeakValueDictionary()

def __new__(self, value: int):
inst = super().__new__(self, value % self.MAX_VALUE)
def __new__(cls, value: int):
inst = super().__new__(cls, value % cls.MAX_VALUE)
return inst

def __repr__(self) -> str:
Expand Down
137 changes: 137 additions & 0 deletions examples/newtype_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from enum import Enum

import pytest

from newtype import NewType, newtype_exclude


class ENV(NewType(str), Enum):

LOCAL = "LOCAL"
DEV = "DEV"
SIT = "SIT"
UAT = "UAT"
PREPROD = "PREPROD"
PROD = "PROD"

class RegularENV(str, Enum):

LOCAL = "LOCAL"
DEV = "DEV"
SIT = "SIT"
UAT = "UAT"
PREPROD = "PREPROD"
PROD = "PROD"

class ENVVariant(str):

__VALID_MEMBERS__ = ["LOCAL", "DEV", "SIT", "UAT", "PREPROD", "PROD"]

def __new__(cls, value: str):
members = ENVVariant.__VALID_MEMBERS__
# if isinstance(value, RollYourOwnNewTypeEnum):
# value_as_str = str(value.value)
# else:
value_as_str = str(value)
if value_as_str not in members:
raise ValueError(f"`value` = {value} must be one of `{members}`; `value_as_str` = {value_as_str}")
return super().__new__(cls, value_as_str)

# why not i write my own `.replace(..)`
# yes, you can but how?
def my_replace(self, old: "ENVVariant", new: "ENVVariant", count: int=-1):
return ENVVariant(str(self).replace(str(old), str(new), count))

class RollYourOwnNewTypeEnum(ENVVariant, Enum):

LOCAL = "LOCAL"
DEV = "DEV"
SIT = "SIT"
UAT = "UAT"
PREPROD = "PREPROD"
PROD = "PROD"


def test_nt_env_replace():

env = ENV.LOCAL

assert env is ENV.LOCAL
assert env is not ENV.DEV
assert isinstance(env, ENV)

# let's say now we want to replace the environment
# nevermind about the reason why we want to do so
env = env.replace(ENV.LOCAL, ENV.DEV)

# replacement is successful
assert env is ENV.DEV
assert env is not ENV.LOCAL

# still an `ENV`
assert isinstance(env, ENV)
assert isinstance(env, str)

with pytest.raises(ValueError):
# cannot replace with something that is not a `ENV`
env = env.replace(ENV.DEV, "NotAnEnv")

with pytest.raises(ValueError):
# cannot even make 'DEV' -> 'dev'
env = env.lower()

def test_reg_env_replace():

env = RegularENV.LOCAL

# expected outcomes
assert env is RegularENV.LOCAL # pass
assert env is not RegularENV.DEV # pass
assert isinstance(env, RegularENV) # pass

# now we try to replace
env = env.replace(RegularENV.LOCAL, RegularENV.DEV)

# we are hoping that it will continue to be a `RegularENV.DEV` but it is not
assert env is not RegularENV.DEV # pass, no longer a `RegularENV`
assert env is not RegularENV.LOCAL # pass, no longer a `RegularENV`
assert not isinstance(env, RegularENV)
assert isinstance(env, str) # 'downcast' (?) to `str`

def test_ryont_env_replace():

env = RollYourOwnNewTypeEnum.LOCAL

# expected outcomes
assert env is RollYourOwnNewTypeEnum.LOCAL # pass
assert env is not RollYourOwnNewTypeEnum.DEV # pass
assert isinstance(env, RollYourOwnNewTypeEnum) # pass

# now we try to replace
env = env.replace(RollYourOwnNewTypeEnum.LOCAL, RollYourOwnNewTypeEnum.DEV)

# we are hoping that it will continue to be a `RollYourOwnNewTypeEnum.DEV` but it is not
assert env is not RollYourOwnNewTypeEnum.DEV # pass, no longer a `RollYourOwnNewTypeEnum`
assert env is not RollYourOwnNewTypeEnum.LOCAL # pass, no longer a `RollYourOwnNewTypeEnum`
assert not isinstance(env, RollYourOwnNewTypeEnum)
assert isinstance(env, str) # 'downcast' (?) to `str`

with pytest.raises(AssertionError):
assert env is RollYourOwnNewTypeEnum.DEV

with pytest.raises(AssertionError):
assert env is RollYourOwnNewTypeEnum.DEV

with pytest.raises(AssertionError):
assert isinstance(env, RollYourOwnNewTypeEnum)

env = env.replace("DEV", "NotAnEnv")
assert env == "NotAnEnv" # this 'shouldn't' pass but it does

env = RollYourOwnNewTypeEnum.LOCAL

# env = env.my_replace(RollYourOwnNewTypeEnum.LOCAL, RollYourOwnNewTypeEnum.PREPROD)

assert isinstance(env, str)
assert env is not RollYourOwnNewTypeEnum.PREPROD
assert isinstance(env, RollYourOwnNewTypeEnum)
Loading
Loading