diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff2445b..62b09e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: CI on: - push: + push: ~ jobs: build: @@ -12,20 +12,44 @@ jobs: python-version: # Note: Use quotes to avoid float cast - especially important if the # version number ends with 0! - - "3.9" - "3.10" - "3.11" + - "3.12" steps: - - name: Clone Repo + - name: Clone repo uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: + # https://github.com/actions/setup-python#caching-packages-dependencies + cache: pip python-version: ${{ matrix.python-version }} - - name: Install Dependencies + - name: Install dependencies run: | python -m pip install --upgrade pip pip install . - - name: Run Tests + - name: Run tests run: python -m unittest + + docs: + runs-on: ubuntu-latest + + steps: + - name: Clone repo + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + # https://github.com/actions/setup-python#caching-packages-dependencies + cache: pip + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[docs-builder] + - name: Check docs build + run: | + cd docs + mkdir -p _static + make html diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..f449c8b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +python: + install: + - requirements: docs/requirements.txt + +sphinx: + configuration: docs/conf.py diff --git a/README.rst b/README.rst index 97481b2..997662c 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +.. image:: https://github.com/todofixthis/filters/actions/workflows/build.yml/badge.svg + :target: https://github.com/todofixthis/filters/actions/workflows/build.yml .. image:: https://readthedocs.org/projects/filters/badge/?version=latest :target: http://filters.readthedocs.io/ @@ -14,8 +16,8 @@ data validation and processing pipelines, including: And much more! -The output from one filter can be "piped" into the input of another, enabling -you to "chain" filters together to quickly and easily create complex data +The output from one filter can be piped into the input of another, enabling you +to chain filters together to quickly and easily create complex data schemas and pipelines. @@ -62,7 +64,7 @@ Parse a JSON string and check that it has correct structure: f.FilterMapper( { 'birthday': f.Date, - 'gender': f.CaseFold | f.Choice(choices={'m', 'f', 'x'}), + 'gender': f.CaseFold | f.Choice(choices={'f', 'm', 'n'}), 'utcOffset': f.Decimal | @@ -81,9 +83,9 @@ Requirements ------------ Filters is known to be compatible with the following Python versions: +- 3.12 - 3.11 - 3.10 -- 3.9 .. note:: I'm only one person, so to keep from getting overwhelmed, I'm only committing diff --git a/docs/index.rst b/docs/index.rst index ac8736c..8dd2622 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -90,9 +90,9 @@ Requirements ------------ Filters is known to be compatible with the following Python versions: +* 3.12 * 3.11 * 3.10 -* 3.9 .. note:: I'm only one person, so to keep from getting overwhelmed, I'm only committing diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..cdf642b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html#id6 +sphinx +sphinx_rtd_theme diff --git a/filters/__init__.py b/filters/__init__.py index 150ca71..3a4a5d1 100644 --- a/filters/__init__.py +++ b/filters/__init__.py @@ -1,8 +1,3 @@ -# http://stackoverflow.com/a/2073599/ -from pkg_resources import require -__version__ = require('phx-filters')[0].version -del require - # Make the base filters accessible from the top level of the package. # Note that the order is important here, due to dependencies. from .base import * diff --git a/filters/extensions.py b/filters/extensions.py index 87b602a..2aab025 100644 --- a/filters/extensions.py +++ b/filters/extensions.py @@ -1,10 +1,10 @@ import typing +from importlib.metadata import EntryPoint, entry_points from inspect import getmembers as get_members, isabstract as is_abstract, \ isclass as is_class, ismodule as is_module from logging import getLogger from class_registry import EntryPointClassRegistry -from pkg_resources import EntryPoint, iter_entry_points from filters.base import BaseFilter @@ -49,6 +49,11 @@ def __init__(self, group: str = GROUP_NAME) -> None: super().__init__(group) def __getattr__(self, item: str) -> typing.Type[BaseFilter]: + """ + Provides attr-like interface for accessing extension filters (the + default interface for class registries is to access items rather than + attributes). + """ return self[item] def __repr__(self): @@ -58,8 +63,7 @@ def _get_cache(self) -> typing.Dict[str, typing.Type[BaseFilter]]: if self._cache is None: self._cache = {} - for target in iter_entry_points( - self.group): # type: EntryPoint + for target in entry_points(group=self.group): # type: EntryPoint filter_ = target.load() ift_result = is_filter_type(filter_) diff --git a/pyproject.toml b/pyproject.toml index c8ce2a3..7879d7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,10 +2,10 @@ [project] name = "phx-filters" -version = "3.3.0" +version = "3.4.0" description = "Validation and data pipelines made easy!" readme = "README.rst" -requires-python = ">= 3.6" +requires-python = ">= 3.10" license = { file = "LICENCE.txt" } authors = [ { email = "Phoenix Zerin " } @@ -23,16 +23,16 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", 'Topic :: Text Processing :: Filters', ] dependencies = [ - "phx-class-registry", + "phx-class-registry >= 4.1.0", "python-dateutil", "pytz", "regex >= 2018.8.17", diff --git a/test/helper.py b/test/helper.py new file mode 100644 index 0000000..9737cc8 --- /dev/null +++ b/test/helper.py @@ -0,0 +1,41 @@ +import sys +from importlib.metadata import DistributionFinder, PathDistribution +from os import path +from pathlib import Path + + +class DummyDistributionFinder(DistributionFinder): + """ + Injects a dummy distribution into the meta path finder, so that we can + pretend like it's been pip installed during unit tests (i.e., so that we + can test ``FilterExtensionRegistry``), without polluting the persistent + virtualenv. + """ + + DUMMY_PACKAGE_DIR = 'filter_extension.egg-info' + + @classmethod + def install(cls): + for finder in sys.meta_path: + if isinstance(finder, cls): + # If we've already installed an instance of the class, then + # something is probably wrong with our tests. + raise ValueError(f'{cls.__name__} is already installed') + + sys.meta_path.append(cls()) + + @classmethod + def uninstall(cls): + for i, finder in enumerate(sys.meta_path): + if isinstance(finder, cls): + sys.meta_path.pop(i) + return + else: + # If we didn't find an installed instance of the class, then + # something is probably wrong with our tests. + raise ValueError(f'{cls.__name__} was not installed') + + def find_distributions(self, context=...) -> list[PathDistribution]: + return [PathDistribution( + Path(path.join(path.dirname(__file__), self.DUMMY_PACKAGE_DIR)) + )] diff --git a/test/test_extensions.py b/test/test_extensions.py index ed94f31..2bbae24 100644 --- a/test/test_extensions.py +++ b/test/test_extensions.py @@ -1,23 +1,18 @@ -from os.path import dirname from unittest import TestCase -from pkg_resources import working_set - from filters.extensions import FilterExtensionRegistry from filters.macros import FilterMacroType from test import TestFilterAlpha, TestFilterBravo +from test.helper import DummyDistributionFinder def setUpModule(): - # - # Install a fake distribution that we can use to inject entry - # points at runtime. - # - # The side effects from this are pretty severe, but they (very - # probably) only impact this test, and they are undone as soon as - # the process terminates. - # - working_set.add_entry(dirname(__file__)) + # Inject a distribution that defines some entry points. + DummyDistributionFinder.install() + + +def tearDownModule(): + DummyDistributionFinder.uninstall() class FilterExtensionRegistryTestCase(TestCase): diff --git a/test/test_string.py b/test/test_string.py index 5e57dc3..5dc0dcf 100644 --- a/test/test_string.py +++ b/test/test_string.py @@ -1290,7 +1290,7 @@ def test_fail_no_match(self): def test_pass_unicode_character_class(self): """ - By default, character classes like ``\w`` will take unicode into + By default, character classes like ``\\w`` will take unicode into account. """ # "Hi, there!" in Japanese, according to the internet :innocent: @@ -1382,7 +1382,7 @@ def test_pass_pattern_split(self): You can also use a regex to split the string. """ self.assertFilterPasses( - self._filter('foo-12-bar-34-baz', pattern='[-\d]+'), + self._filter('foo-12-bar-34-baz', pattern=r'[-\d]+'), ['foo', 'bar', 'baz'], ) @@ -1393,7 +1393,7 @@ def test_pass_pattern_split_with_groups(self): """ self.assertFilterPasses( # Note grouping parentheses in the regex. - self._filter('foo-12-bar-34-baz', pattern='([-\d]+)'), + self._filter('foo-12-bar-34-baz', pattern=r'([-\d]+)'), ['foo', '-12-', 'bar', '-34-', 'baz'], ) @@ -1405,7 +1405,7 @@ def test_pass_no_split(self): parts. """ self.assertFilterPasses( - self._filter('foo:bar:baz', pattern='[-\d]+'), + self._filter('foo:bar:baz', pattern=r'[-\d]+'), ['foo:bar:baz'], ) @@ -1659,7 +1659,7 @@ def test_pass_curly_hex(self): """ You can include curly braces around hex values. - Use ``Regex(r'^[\da-f]+$') | Uuid`` if you only want to allow plain + Use ``Regex(r'^[\\da-f]+$') | Uuid`` if you only want to allow plain hex. """ filtered = self._filter('{54d6ebf8a3f55ed59becdedfb3b0773f}') @@ -1678,7 +1678,7 @@ def test_pass_urn(self): antiquated, but still valid. If you want to prohibit URNs, chain this filter with - ``Regex(r'^[\da-f]+$')``. + ``Regex(r'^[\\da-f]+$')``. References: @@ -1764,7 +1764,7 @@ def test_pass_unicode(self): """ The incoming value is a unicode. """ - self.assertFilterPasses('┻━┻︵ \(°□°)/ ︵ ┻━┻ ') # RAWR! + self.assertFilterPasses(r'┻━┻︵ \(°□°)/ ︵ ┻━┻ ') # RAWR! def test_pass_bytes_utf8(self): """ diff --git a/tox.ini b/tox.ini index aefb8bb..fb2d97e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{11,10,9} +envlist = py3{12,11,10} [testenv] commands = python -m unittest