diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9b979d..c8c2cd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,13 +16,11 @@ jobs: - macos-latest python: - - "3.7" - - "3.8" - - "3.9" - "3.10" - "3.11" - "3.12" - - "pypy3.9" + - "3.13" + - "3.14" steps: - uses: actions/checkout@v4 @@ -34,15 +32,12 @@ jobs: - name: Install run: | - pip install coverage - pip install -e .[test] + make install - name: Run tests run: | - python --version - python -m plumber.tests.__init__ + make test - name: Run coverage run: | - coverage run -m plumber.tests.__init__ - coverage report --fail-under=98 + make coverage diff --git a/.gitignore b/.gitignore index 584028d..845d095 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /htmlcov/ /requirements-mxdev.txt /venv +/build diff --git a/CHANGES.rst b/CHANGES.rst index a659f04..cd930be 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,11 @@ Changes 1.8 (unreleased) ---------------- -- Nothing changed yet. +- Refactor package layout to use ``pyproject.toml`` and implicit namespace packages. + [rnix] + +- Drop Python 2 support. + [rnix] 1.7 (2022-03-17) diff --git a/LICENSE.rst b/LICENSE.rst index 64b6915..2e9776a 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -2,7 +2,7 @@ License ======= Copyright (c) 2011-2021, BlueDynamics Alliance, Austria, Germany, Switzerland -Copyright (c) 2021-2022, Node Contributors +Copyright (c) 2021-2025, Node Contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 40c2444..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include *.rst -recursive-include src * -recursive-exclude src *.pyc *.pyo -prune scripts diff --git a/Makefile b/Makefile index addf311..5debf0f 100644 --- a/Makefile +++ b/Makefile @@ -32,18 +32,42 @@ CLEAN_FS?= # Default: include.mk INCLUDE_MAKEFILE?=include.mk +# Optional additional directories to be added to PATH in format +# `/path/to/dir/:/path/to/other/dir`. Gets inserted first, thus gets searched +# first. +# No default value. +EXTRA_PATH?= + ## core.mxenv -# Python interpreter to use. +# Primary Python interpreter to use. It is used to create the +# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. +# If global `uv` is used, this value is passed as `--python VALUE` to the venv creation. +# uv then downloads the Python interpreter if it is not available. +# for more on this feature read the [uv python documentation](https://docs.astral.sh/uv/concepts/python-versions/) # Default: python3 -PYTHON_BIN?=python3 +PRIMARY_PYTHON?=python3 # Minimum required Python version. -# Default: 3.7 -PYTHON_MIN_VERSION?=3.7 +# Default: 3.9 +PYTHON_MIN_VERSION?=3.10 + +# Install packages using the given package installer method. +# Supported are `pip` and `uv`. If uv is used, its global availability is +# checked. Otherwise, it is installed, either in the virtual environment or +# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If +# `VENV_ENABLED` and uv is selected, uv is used to create the virtual +# environment. +# Default: pip +PYTHON_PACKAGE_INSTALLER?=uv + +# Flag whether to use a global installed 'uv' or install +# it in the virtual environment. +# Default: false +MXENV_UV_GLOBAL?=false # Flag whether to use virtual environment. If `false`, the -# interpreter according to `PYTHON_BIN` found in `PATH` is used. +# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. # Default: true VENV_ENABLED?=true @@ -58,7 +82,7 @@ VENV_CREATE?=true # target folder for the virtual environment. If `VENV_ENABLED` is `true` and # `VENV_CREATE` is false it is expected to point to an existing virtual # environment. If `VENV_ENABLED` is `false` it is ignored. -# Default: venv +# Default: .venv VENV_FOLDER?=venv # mxdev to install in virtual environment. @@ -81,12 +105,19 @@ RUFF_SRC?=src # Default: mx.ini PROJECT_CONFIG?=mx.ini +## core.packages + +# Allow prerelease and development versions. +# By default, the package installer only finds stable versions. +# Default: false +PACKAGES_ALLOW_PRERELEASES?=false + ## qa.test # The command which gets executed. Defaults to the location the # :ref:`run-tests` template gets rendered to if configured. # Default: .mxmake/files/run-tests.sh -TEST_COMMAND?=$(VENV_FOLDER)/bin/python -m plumber.tests.__init__ +TEST_COMMAND?=pytest tests # Additional Python requirements for running tests to be # installed (via pip). @@ -103,9 +134,10 @@ TEST_DEPENDENCY_TARGETS?= # :ref:`run-coverage` template gets rendered to if configured. # Default: .mxmake/files/run-coverage.sh COVERAGE_COMMAND?=\ - $(VENV_FOLDER)/bin/coverage run \ - -m plumber.tests.__init__ \ - && $(VENV_FOLDER)/bin/coverage report --fail-under=98 + coverage run \ + --source=src/plumber \ + -m pytest tests \ + && coverage report --fail-under=99 ############################################################################## # END SETTINGS - DO NOT EDIT BELOW THIS LINE @@ -119,6 +151,8 @@ CHECK_TARGETS?= TYPECHECK_TARGETS?= FORMAT_TARGETS?= +export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) + # Defensive settings for make: https://tech.davis-hansson.com/p/make/ SHELL:=bash .ONESHELL: @@ -135,7 +169,7 @@ MXMAKE_FOLDER?=.mxmake # Sentinel files SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels SENTINEL?=$(SENTINEL_FOLDER)/about.txt -$(SENTINEL): +$(SENTINEL): $(firstword $(MAKEFILE_LIST)) @mkdir -p $(SENTINEL_FOLDER) @echo "Sentinels for the Makefile process." > $(SENTINEL) @@ -143,40 +177,62 @@ $(SENTINEL): # mxenv ############################################################################## -# Check if given Python is installed -ifeq (,$(shell which $(PYTHON_BIN))) -$(error "PYTHON=$(PYTHON_BIN) not found in $(PATH)") -endif +OS?= -# Check if given Python version is ok -PYTHON_VERSION_OK=$(shell $(PYTHON_BIN) -c "import sys; print((int(sys.version_info[0]), int(sys.version_info[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))))") -ifeq ($(PYTHON_VERSION_OK),0) -$(error "Need Python >= $(PYTHON_MIN_VERSION)") +# Determine the executable path +ifeq ("$(VENV_ENABLED)", "true") +export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin endif - -# Check if venv folder is configured if venv is enabled -ifeq ($(shell [[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] && echo "true"),"true") -$(error "VENV_FOLDER must be configured if VENV_ENABLED is true") +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) endif -# determine the executable path -ifeq ("$(VENV_ENABLED)", "true") -MXENV_PATH=$(VENV_FOLDER)/bin/ +# Determine the package installer +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip else -MXENV_PATH= +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip endif MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel $(MXENV_TARGET): $(SENTINEL) +ifneq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ + && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : +else + @echo "Use Python $(PYTHON_MIN_VERSION) over uv" +endif + @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ + && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : + @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : ifeq ("$(VENV_ENABLED)", "true") ifeq ("$(VENV_CREATE)", "true") - @echo "Setup Python Virtual Environment under '$(VENV_FOLDER)'" - @$(PYTHON_BIN) -m venv $(VENV_FOLDER) +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") + @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" + @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +else + @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" + @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) + @$(MXENV_PYTHON) -m ensurepip -U +endif +endif +else + @echo "Using system Python interpreter" endif +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + @echo "Install uv" + @$(MXENV_PYTHON) -m pip install uv endif - @$(MXENV_PATH)pip install -U pip setuptools wheel - @$(MXENV_PATH)pip install -U $(MXDEV) - @$(MXENV_PATH)pip install -U $(MXMAKE) + @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel + @echo "Install/Update MXStack Python packages" + @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) @touch $(MXENV_TARGET) .PHONY: mxenv @@ -193,8 +249,8 @@ ifeq ("$(VENV_CREATE)", "true") @rm -rf $(VENV_FOLDER) endif else - @$(MXENV_PATH)pip uninstall -y $(MXDEV) - @$(MXENV_PATH)pip uninstall -y $(MXMAKE) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) endif INSTALL_TARGETS+=mxenv @@ -208,18 +264,18 @@ CLEAN_TARGETS+=mxenv-clean RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel $(RUFF_TARGET): $(MXENV_TARGET) @echo "Install Ruff" - @$(MXENV_PATH)pip install ruff + @$(PYTHON_PACKAGE_COMMAND) install ruff @touch $(RUFF_TARGET) .PHONY: ruff-check ruff-check: $(RUFF_TARGET) @echo "Run ruff check" - @$(MXENV_PATH)ruff check $(RUFF_SRC) + @ruff check $(RUFF_SRC) .PHONY: ruff-format ruff-format: $(RUFF_TARGET) @echo "Run ruff format" - @$(MXENV_PATH)ruff format $(RUFF_SRC) + @ruff format $(RUFF_SRC) .PHONY: ruff-dirty ruff-dirty: @@ -227,7 +283,8 @@ ruff-dirty: .PHONY: ruff-clean ruff-clean: ruff-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y ruff || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y ruff || : + @rm -rf .ruff_cache INSTALL_TARGETS+=$(RUFF_TARGET) CHECK_TARGETS+=ruff-check @@ -247,13 +304,11 @@ MXMAKE_FILES?=$(MXMAKE_FOLDER)/files # set environment variables for mxmake define set_mxfiles_env - @export MXMAKE_MXENV_PATH=$(1) - @export MXMAKE_FILES=$(2) + @export MXMAKE_FILES=$(1) endef # unset environment variables for mxmake define unset_mxfiles_env - @unset MXMAKE_MXENV_PATH @unset MXMAKE_FILES endef @@ -270,9 +325,9 @@ FILES_TARGET:=requirements-mxdev.txt $(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) @echo "Create project files" @mkdir -p $(MXMAKE_FILES) - $(call set_mxfiles_env,$(MXENV_PATH),$(MXMAKE_FILES)) - @$(MXENV_PATH)mxdev -n -c $(PROJECT_CONFIG) - $(call unset_mxfiles_env,$(MXENV_PATH),$(MXMAKE_FILES)) + $(call set_mxfiles_env,$(MXMAKE_FILES)) + @mxdev -n -c $(PROJECT_CONFIG) + $(call unset_mxfiles_env) @test -e $(MXMAKE_FILES)/pip.conf && cp $(MXMAKE_FILES)/pip.conf $(VENV_FOLDER)/pip.conf || : @touch $(FILES_TARGET) @@ -301,11 +356,21 @@ ADDITIONAL_SOURCES_TARGETS?= INSTALLED_PACKAGES=$(MXMAKE_FILES)/installed.txt +ifeq ("$(PACKAGES_ALLOW_PRERELEASES)","true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PACKAGES_PRERELEASES=--prerelease=allow +else +PACKAGES_PRERELEASES=--pre +endif +else +PACKAGES_PRERELEASES= +endif + PACKAGES_TARGET:=$(INSTALLED_PACKAGES) $(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS) @echo "Install python packages" - @$(MXENV_PATH)pip install -r $(FILES_TARGET) - @$(MXENV_PATH)pip freeze > $(INSTALLED_PACKAGES) + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) @touch $(PACKAGES_TARGET) .PHONY: packages @@ -318,8 +383,8 @@ packages-dirty: .PHONY: packages-clean packages-clean: @test -e $(FILES_TARGET) \ - && test -e $(MXENV_PATH)pip \ - && $(MXENV_PATH)pip uninstall -y -r $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ || : @rm -f $(PACKAGES_TARGET) @@ -334,14 +399,14 @@ CLEAN_TARGETS+=packages-clean TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel $(TEST_TARGET): $(MXENV_TARGET) @echo "Install $(TEST_REQUIREMENTS)" - @$(MXENV_PATH)pip install $(TEST_REQUIREMENTS) + @$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS) @touch $(TEST_TARGET) .PHONY: test test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS) - @echo "Run tests" - @test -z "$(TEST_COMMAND)" && echo "No test command defined" - @test -z "$(TEST_COMMAND)" || bash -c "$(TEST_COMMAND)" + @test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || : + @echo "Run tests using $(TEST_COMMAND)" + @/usr/bin/env bash -c "$(TEST_COMMAND)" .PHONY: test-dirty test-dirty: @@ -349,7 +414,7 @@ test-dirty: .PHONY: test-clean test-clean: test-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y $(TEST_REQUIREMENTS) || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : @rm -rf .pytest_cache INSTALL_TARGETS+=$(TEST_TARGET) @@ -363,14 +428,14 @@ DIRTY_TARGETS+=test-dirty COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel $(COVERAGE_TARGET): $(TEST_TARGET) @echo "Install Coverage" - @$(MXENV_PATH)pip install -U coverage + @$(PYTHON_PACKAGE_COMMAND) install -U coverage @touch $(COVERAGE_TARGET) .PHONY: coverage coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) - @echo "Run coverage" - @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" - @test -z "$(COVERAGE_COMMAND)" || bash -c "$(COVERAGE_COMMAND)" + @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : + @echo "Run coverage using $(COVERAGE_COMMAND)" + @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" .PHONY: coverage-dirty coverage-dirty: @@ -378,13 +443,17 @@ coverage-dirty: .PHONY: coverage-clean coverage-clean: coverage-dirty - @test -e $(MXENV_PATH)pip && $(MXENV_PATH)pip uninstall -y coverage || : + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : @rm -rf .coverage htmlcov INSTALL_TARGETS+=$(COVERAGE_TARGET) DIRTY_TARGETS+=coverage-dirty CLEAN_TARGETS+=coverage-clean +############################################################################## +# Custom includes +############################################################################## + -include $(INCLUDE_MAKEFILE) ############################################################################## diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..af525ec --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "plumber" +version = "1.8.dev0" +description = "An alternative to mixin-based extension of classes." +readme = "README.rst" +license = {text = "Simplified BSD"} +authors = [{name = "Node Contributors", email = "dev@conestack.org"}] +keywords = ["class", "extension", "mixin", "alternative"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: Python Software Foundation License", + "Operating System :: OS Independent", + "Topic :: Software Development", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] + +[project.optional-dependencies] +test = [ + "zope.interface", +] + +[project.urls] +Homepage = "http://github.com/conestack/plumber" + +[tool.hatch.build.targets.wheel] +packages = ["src/plumber"] + +[tool.zest-releaser] +create-wheel = true + +[tool.ruff] +target-version = "py310" + +[tool.ruff.format] +quote-style = "single" diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 5391c72..0000000 --- a/ruff.toml +++ /dev/null @@ -1,4 +0,0 @@ -target-version = "py37" - -[format] -quote-style = "single" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 81049c3..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[zest.releaser] -create-wheel = yes diff --git a/setup.py b/setup.py deleted file mode 100644 index 7ee3df4..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -from setuptools import find_packages -from setuptools import setup -import os - - -def read_file(name): - with open(os.path.join(os.path.dirname(__file__), name)) as f: - return f.read() - - -version = '1.8.dev0' -shortdesc = 'An alternative to mixin-based extension of classes.' -longdesc = '\n\n'.join([read_file(name) for name in [ - 'README.rst', - 'CHANGES.rst', - 'LICENSE.rst' -]]) - - -setup( - name='plumber', - version=version, - description=shortdesc, - long_description=longdesc, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: Python Software Foundation License', - 'Operating System :: OS Independent', - 'Topic :: Software Development', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10' - ], - keywords='class extension mixin alternative', - author='Node Contributors', - author_email='dev@conestack.org', - url='http://github.com/conestack/plumber', - license='Simplified BSD', - packages=find_packages('src'), - package_dir={'': 'src'}, - include_package_data=True, - zip_safe=True, - install_requires=['setuptools'], - extras_require=dict(test=['zope.interface']), - test_suite='plumber.tests', - entry_points=""" - """ -) diff --git a/src/plumber/__init__.py b/src/plumber/__init__.py index 03eddc3..8f40049 100644 --- a/src/plumber/__init__.py +++ b/src/plumber/__init__.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import # noqa from .behavior import Behavior # noqa from .exceptions import PlumbingCollision # noqa from .instructions import default # noqa diff --git a/src/plumber/behavior.py b/src/plumber/behavior.py index d94f70f..d195eb1 100644 --- a/src/plumber/behavior.py +++ b/src/plumber/behavior.py @@ -1,11 +1,8 @@ -from __future__ import absolute_import -from plumber.compat import ITER_FUNC -from plumber.compat import add_metaclass -from plumber.instructions import Instruction -from plumber.instructions import plumb +from .instructions import Instruction +from .instructions import plumb try: - from plumber.instructions import _implements + from .instructions import _implements ZOPE_INTERFACE_AVAILABLE = True except ImportError: # pragma: no cover @@ -51,22 +48,18 @@ class behaviormetaclass(type): >>> from plumber.behavior import Behavior >>> from plumber.behavior import behaviormetaclass - >>> from plumber.compat import add_metaclass - >>> @add_metaclass(behaviormetaclass) - ... class A(object): + ... class A(object, metaclass=behaviormetaclass): ... pass >>> getattr(A, '__plumbing_instructions__', 'No behavior') 'No behavior' - >>> @add_metaclass(behaviormetaclass) - ... class A(Behavior): + ... class A(Behavior, metaclass=behaviormetaclass): ... pass >>> getattr(A, '__plumbing_instructions__', None) and 'Behavior' 'Behavior' - """ def __init__(cls, name, bases, dct): @@ -87,7 +80,7 @@ def __init__(cls, name, bases, dct): if ZOPE_INTERFACE_AVAILABLE: instructions.append(_implements(cls)) - for name, item in getattr(cls.__dict__, ITER_FUNC)(): + for name, item in cls.__dict__.items(): # adopt instructions and enlist them if isinstance(item, Instruction): item.__name__ = name @@ -113,6 +106,5 @@ def __init__(cls, name, bases, dct): # Base class for plumbing behaviors: identification and metaclass setting # No doctest allowed here, it would be recognized as an instruction. -@add_metaclass(behaviormetaclass) -class Behavior(_Behavior): +class Behavior(_Behavior, metaclass=behaviormetaclass): pass diff --git a/src/plumber/compat.py b/src/plumber/compat.py deleted file mode 100644 index 3191b90..0000000 --- a/src/plumber/compat.py +++ /dev/null @@ -1,30 +0,0 @@ -# Python compatibility support code -import sys - - -IS_PY2 = sys.version_info[0] < 3 -IS_PYPY = '__pypy__' in sys.builtin_module_names -ITER_FUNC = 'iteritems' if IS_PY2 else 'items' -STR_TYPE = basestring if IS_PY2 else str # noqa - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - - # This is taken from six - def wrapper(cls): - orig_vars = cls.__dict__.copy() - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - slots = orig_vars.get('__slots__') - # since add_metaclass is only used for behaviormetaclass, slots related - # code gets never called. we keep this snippet from the original - # code anyway and mark coverage report to ignore it. - if slots is not None: - if isinstance(slots, str): # pragma: no cover - slots = [slots] - for slots_var in slots: # pragma: no cover - orig_vars.pop(slots_var) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - - return wrapper diff --git a/src/plumber/instructions.py b/src/plumber/instructions.py index d7050c5..e3e2300 100644 --- a/src/plumber/instructions.py +++ b/src/plumber/instructions.py @@ -1,6 +1,4 @@ -from __future__ import absolute_import -from plumber.compat import STR_TYPE -from plumber.exceptions import PlumbingCollision +from .exceptions import PlumbingCollision try: from zope.interface import classImplements @@ -30,7 +28,6 @@ def payload(item): >>> payload(Instruction(Instruction(Foo))) is Foo True - """ if not isinstance(item, Instruction): return item @@ -99,7 +96,6 @@ def plumb_str(leftdoc, rightdoc): >>> plumb_str(None, None) is None True - """ if leftdoc is None: return rightdoc @@ -540,10 +536,9 @@ def ok(self, p1, p2): with: - """ - if isinstance(p1, STR_TYPE): - return isinstance(p2, STR_TYPE) or p2 is None + if isinstance(p1, str): + return isinstance(p2, str) or p2 is None if isinstance(p1, property): return isinstance(p2, property) if callable(p1): @@ -551,7 +546,7 @@ def ok(self, p1, p2): return False def plumb(self, plbfunc, p1, p2): - if isinstance(p1, STR_TYPE): + if isinstance(p1, str): return plumb_str(p1, p2) if isinstance(p1, property): # XXX: This should be split up into instructions during part @@ -634,7 +629,6 @@ class _implements(Stage2Instruction): <_implements '__interfaces__' of None payload=('foo',)> with: - """ __name__ = '__interfaces__' diff --git a/src/plumber/plumber.py b/src/plumber/plumber.py index d84d85c..a489bc8 100644 --- a/src/plumber/plumber.py +++ b/src/plumber/plumber.py @@ -1,5 +1,4 @@ -from __future__ import absolute_import -from plumber.behavior import Instructions +from .behavior import Instructions class Stacks(object): diff --git a/src/plumber/tests/__init__.py b/src/plumber/tests/__init__.py deleted file mode 100644 index 75e035d..0000000 --- a/src/plumber/tests/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -from plumber import compat -from pprint import pprint -import doctest -import sys -import unittest - - -optionflags = ( - doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS | doctest.REPORT_ONLY_FIRST_FAILURE -) - - -if not compat.IS_PY2 and not compat.IS_PYPY: # pragma: no cover - TESTFILES = ['../../../README.rst', '../behavior.py', '../instructions.py'] -else: # pragma: no cover - TESTFILES = [] - - -def test_suite(): - from plumber.tests import test_plumber - - suite = unittest.TestSuite() - suite.addTest(unittest.findTestCases(test_plumber)) - suite.addTests( - [ - doctest.DocFileSuite( - testfile, globs=dict(pprint=pprint), optionflags=optionflags - ) - for testfile in TESTFILES - ] - ) - return suite - - -if __name__ == '__main__': - runner = unittest.TextTestRunner(failfast=True) - result = runner.run(test_suite()) - sys.exit(not result.wasSuccessful()) diff --git a/src/plumber/tests/globalmetaclass.py b/src/plumber/tests/globalmetaclass.py deleted file mode 100644 index 7437c22..0000000 --- a/src/plumber/tests/globalmetaclass.py +++ /dev/null @@ -1,94 +0,0 @@ -"""A module to test setting a metaclass globally - -ATTENTION: we do not recommend this, but you can do it! - -Mostly here for understanding what's going on. -""" -from plumber import Behavior -from plumber import plumber -from plumber import plumbing -from zope.interface import Interface -from zope.interface import implementer - - -__metaclass__ = plumber - - -class IBehavior1(Interface): - """A zope.interface.Interface is not affected by the global - ``__metaclass__``. - - .. code-block:: pycon - - >>> IBehavior1.__class__ - - - """ - - -class Foo: - """A global meta-class declaration makes all classes at least new-style - classes, even when not subclassing subclasses. - - .. code-block:: pycon - - >>> Foo.__class__ - - - >>> issubclass(Foo, object) - True - - """ - - -@implementer(IBehavior1) -class Behavior1(Behavior): - pass - - -class ClassMaybeUsingAPlumbing(object): - """If subclassing object, the global metaclass declaration is ignored. - - .. code-block:: pycon - - >>> ClassMaybeUsingAPlumbing.__class__ - - - """ - - -@plumbing(Behavior1) -class ClassReallyUsingAPlumbing: - """A plumbing class. - - .. code-block:: pycon - - >>> ClassReallyUsingAPlumbing.__class__ - - - >>> issubclass(ClassReallyUsingAPlumbing, object) - True - - >>> IBehavior1.implementedBy(ClassReallyUsingAPlumbing) - True - - """ - - -class BCClassReallyUsingAPlumbing: - """A plumbing class setting behaviors the B/C method. - - .. code-block:: pycon - - >>> BCClassReallyUsingAPlumbing.__class__ - - - >>> issubclass(BCClassReallyUsingAPlumbing, object) - True - - >>> IBehavior1.implementedBy(BCClassReallyUsingAPlumbing) - True - - """ - - __plumbing__ = Behavior1 diff --git a/src/plumber/tests/test_plumber.py b/tests/test_plumber.py similarity index 92% rename from src/plumber/tests/test_plumber.py rename to tests/test_plumber.py index 657da4c..79f72b1 100644 --- a/src/plumber/tests/test_plumber.py +++ b/tests/test_plumber.py @@ -8,16 +8,13 @@ from plumber import plumbifexists from plumber import plumbing from plumber.behavior import behaviormetaclass -from plumber.compat import add_metaclass from plumber.instructions import Instruction from plumber.instructions import _implements from plumber.instructions import payload from plumber.instructions import plumb_str from zope.interface import Interface from zope.interface import implementer -from zope.interface.interface import InterfaceClass import inspect -import sys import unittest @@ -229,16 +226,14 @@ def test_implements(self): class TestBehavior(unittest.TestCase): def test_behaviormetaclass(self): - @add_metaclass(behaviormetaclass) - class A(object): + class A(object, metaclass=behaviormetaclass): pass self.assertEqual( getattr(A, '__plumbing_instructions__', 'No behavior'), 'No behavior' ) - @add_metaclass(behaviormetaclass) - class B(Behavior): + class B(Behavior, metaclass=behaviormetaclass): pass self.assertEqual( @@ -258,35 +253,6 @@ class B(A): self.assertFalse('bar' in plumber.derived_members((B,))) -class TestGlobalMetaclass(unittest.TestCase): - @unittest.skipIf( - sys.version_info[0] >= 3, - '__metaclass__ attribute on module leven only works in python 2', - ) - def test_global_metaclass(self): - from plumber.tests import globalmetaclass as gm - - # A zope.interface.Interface is not affected by the global - # ``__metaclass__``. - self.assertEqual(gm.IBehavior1.__class__, InterfaceClass) - - # A global meta-class declaration makes all classes at least new-style - # classes, even when not subclassing subclasses - self.assertEqual(gm.Foo.__class__, plumber) - self.assertTrue(issubclass(gm.Foo, object)) - - # If subclassing object, the global metaclass declaration is ignored:: - self.assertEqual(gm.ClassMaybeUsingAPlumbing.__class__, type) - - self.assertEqual(gm.ClassReallyUsingAPlumbing.__class__, plumber) - self.assertTrue(issubclass(gm.ClassReallyUsingAPlumbing, object)) - self.assertTrue(gm.IBehavior1.implementedBy(gm.ClassReallyUsingAPlumbing)) - - self.assertEqual(gm.BCClassReallyUsingAPlumbing.__class__, plumber) - self.assertTrue(issubclass(gm.BCClassReallyUsingAPlumbing, object)) - self.assertTrue(gm.IBehavior1.implementedBy(gm.BCClassReallyUsingAPlumbing)) - - class TestMetaclassHooks(unittest.TestCase): def test_metaclasshook(self): class IBehaviorInterface(Interface): @@ -368,20 +334,6 @@ class Sub(Plumbing): stage2 = stacks.stage2 self.assertEqual(sorted(list(stage2.keys())), ['__interfaces__']) - @unittest.skipIf( - sys.version_info[0] >= 3, '__metaclass__ property only works in python 2' - ) - def test_bc_plumbing_py2(self): - class Behavior1(Behavior): - a = default(True) - - class BCPlumbing(object): - __metaclass__ = plumber - __plumbing__ = Behavior1 - - plb = BCPlumbing() - self.assertTrue(plb.a) - class TestPlumberStage1(unittest.TestCase): def test_finalize_instruction(self):