From ab141de085462f930c5fbf4fdad0ef4c29b0b6eb Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Mon, 11 Nov 2024 10:06:16 -0600 Subject: [PATCH] Update supported Python versions; use native ns packages. --- .coveragerc | 19 ++- .github/dependabot.yml | 13 ++ .github/workflows/tests.yml | 62 ++++++++ .pylintrc | 173 ++++++++++++++++++---- CHANGES.rst | 5 +- pyproject.toml | 5 + setup.cfg | 8 - setup.py | 26 ++-- src/nti/__init__.py | 2 +- src/nti/traversal/compat.py | 90 ++++------- src/nti/traversal/tests/test_compat.py | 8 +- src/nti/traversal/tests/test_traversal.py | 41 +++-- src/nti/traversal/traversal.py | 34 ++--- tox.ini | 40 ++--- 14 files changed, 351 insertions(+), 175 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/tests.yml create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/.coveragerc b/.coveragerc index fc99c73..5a6e7ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,25 @@ [run] -source = nti.traversal +source_pkgs = nti.traversal +relative_files = True + [report] +# Coverage is run on Linux under cPython 2 and 3, +# exclude branches that are windows, pypy +# specific exclude_lines = pragma: no cover + def __repr__ + raise AssertionError raise NotImplementedError if __name__ == .__main__.: + if PYPY: + if sys.platform == 'win32': + if mswindows: + if is_windows: +fail_under = 99.0 +precision = 2 + +# Local Variables: +# mode: conf +# End: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0b84a24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: monthly diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3765040 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,62 @@ +name: tests + +on: [push, pull_request] + +env: + PYTHONHASHSEED: 1042466059 + ZOPE_INTERFACE_STRICT_IRO: 1 + + +jobs: + test: + strategy: + matrix: + python-version: + - "pypy-3.10" + - "3.11" + - "3.12" + - "3.13" + extras: + - "[test,docs]" + # include: + # - python-version: "3.13" + # extras: "[test,docs,gevent,pyramid]" + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'setup.py' + - name: Install dependencies + run: | + python -m pip install -U pip setuptools wheel + python -m pip install -U coverage + python -m pip install -v -U -e ".${{ matrix.extras }}" + - name: Test + run: | + python -m coverage run -m zope.testrunner --test-path=src --auto-color --auto-progress + coverage run -a -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctests + coverage combine || true + coverage report -i || true + - name: Lint + if: matrix.python-version == '3.12' + run: | + python -m pip install -U pylint + pylint nti.site + - name: Submit to Coveralls + uses: coverallsapp/github-action@v2 + with: + parallel: true + + coveralls_finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true diff --git a/.pylintrc b/.pylintrc index 204b5ae..814d8a3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,72 @@ +[MASTER] +load-plugins=pylint.extensions.bad_builtin, + pylint.extensions.check_elif, + pylint.extensions.code_style, + pylint.extensions.dict_init_mutate, + pylint.extensions.docstyle, + pylint.extensions.dunder, + pylint.extensions.comparison_placement, + pylint.extensions.confusing_elif, + pylint.extensions.for_any_all, + pylint.extensions.consider_refactoring_into_while_condition, + pylint.extensions.mccabe, + pylint.extensions.eq_without_hash, + pylint.extensions.redefined_variable_type, + pylint.extensions.overlapping_exceptions, + pylint.extensions.docparams, + pylint.extensions.private_import, + pylint.extensions.set_membership, + pylint.extensions.typing, + +# magic_value wants you to not use arbitrary strings and numbers +# inline in the code. But it's overzealous and has way too many false +# positives. Trust people to do the most readable thing. +# pylint.extensions.magic_value + +# Empty comment would be good, except it detects blank lines within +# a single comment block. +# +# Those are often used to separate paragraphs, like here. +# pylint.extensions.empty_comment, + +# consider_ternary_expression is a nice check, but is also overzealous. +# Trust the human to do the readable thing. +# pylint.extensions.consider_ternary_expression, + +# redefined_loop_name tends to catch us with things like +# for name in (a, b, c): name = name + '_column' ... +# pylint.extensions.redefined_loop_name, + +# This wants you to turn ``x in (1, 2)`` into ``x in {1, 2}``. +# They both result in the LOAD_CONST bytecode, one a tuple one a +# frozenset. In theory a set lookup using hashing is faster than +# a linear scan of a tuple; but if the tuple is small, it can often +# actually be faster to scan the tuple. +# pylint.extensions.set_membership, + +# Fix zope.cachedescriptors.property.Lazy; the property-classes doesn't seem to +# do anything. +# https://stackoverflow.com/questions/51160955/pylint-how-to-specify-a-self-defined-property-decorator-with-property-classes +# For releases prior to 2.14.2, this needs to be a one-line, quoted string. After that, +# a multi-line string. +# - Make zope.cachedescriptors.property.Lazy look like a property; +# fixes pylint thinking it is a method. +# - Run in Pure Python mode (ignore C extensions that respect this); +# fixes some issues with zope.interface, like IFoo.providedby(ob) +# claiming not to have the right number of parameters...except no, it does not. +init-hook = + import astroid.bases + astroid.bases.POSSIBLE_PROPERTIES.add('Lazy') + astroid.bases.POSSIBLE_PROPERTIES.add('LazyOnClass') + astroid.bases.POSSIBLE_PROPERTIES.add('readproperty') + astroid.bases.POSSIBLE_PROPERTIES.add('non_overridable') + import os + os.environ['PURE_PYTHON'] = ("1") + # Ending on a quoted string + # breaks pylint 2.14.5 (it strips the trailing quote. This is + # probably because it tries to handle one-line quoted strings as well as multi-blocks). + # The parens around it fix the issue. + [MESSAGES CONTROL] @@ -11,7 +80,7 @@ # mixing) -# invalid-name, ; Most commonly for `logger`, which it wants to be LOGGER +# invalid-name, ; We get lots of these, especially in scripts. should fix many of them # protected-access, ; We have many cases of this; legit ones need to be examinid and commented, then this removed # no-self-use, ; common in superclasses with extension points # too-few-public-methods, ; Exception and marker classes get tagged with this @@ -19,6 +88,7 @@ # global-statement, ; should tag individual instances # multiple-statements, ; "from gevent import monkey; monkey.patch_all()" # locally-disabled, ; yes, we know we're doing this. don't replace one warning with another +# cyclic-import, ; most of these are deferred imports # too-many-arguments, ; these are almost always because that's what the stdlib does # redefined-builtin, ; likewise: these tend to be keyword arguments like len= in the stdlib # undefined-all-variable, ; XXX: This crashes with pylint 1.5.4 on Travis (but not locally on Py2/3 @@ -27,70 +97,123 @@ # see https://github.com/PyCQA/pylint/issues/846 # useless-suppression: the only way to avoid repeating it for specific statements everywhere that we # do Py2/Py3 stuff is to put it here. Sadly this means that we might get better but not realize it. -# too-many-ancestors: inheriting from Zope objects gets that. -# assignment-from-no-return: Interfaces do this in Python 3. -disable=missing-docstring, +# duplicate-code: Yeah, the compatibility ssl modules are much the same +# In pylint 1.8.0, inconsistent-return-statements are created for the wrong reasons. +# This code raises it, even though there is only one return (the implicit ``return None`` is presumably +# what triggers it): +# def foo(): +# if baz: +# return 1 +# In Pylint 2dev1, needed for Python 3.7, we get spurious "useless return" errors: +# @property +# def foo(self): +# return None # generates useless-return +# Pylint 2.4 adds import-outside-toplevel. But we do that a lot to defer imports because of patching. +# Pylint 2.4 adds self-assigning-variable. But we do *that* to avoid unused-import when we +# "export" the variable and dont have a __all__. +# Pylint 2.6+ adds some python-3-only things that dont apply: raise-missing-from, super-with-arguments, consider-using-f-string, redundant-u-string-prefix +# cyclic import is added because it pylint is spuriously detecting that +# consider-using-assignment-expr wants you to transform things like: +# foo = get_foo() +# if foo: ... +# +# Into ``if (foo := get_foo()):`` +# But there are a *lot* of those. Trust people to do the right, most +# readable, thing +# +# docstring-first-line-empty: That's actually our standard, based on Django. +# XXX: unclear on the docstring warnings, missing-type-doc, missing-param-doc, +# differing-param-doc, differing-type-doc (are the last two replacements for the first two?) +# +# They should be addressed, in general they are a good thing, but sometimes they are +# unnecessary. +disable=wrong-import-position, + wrong-import-order, + missing-docstring, + ungrouped-imports, invalid-name, - protected-access, - no-self-use, too-few-public-methods, - exec-used, global-statement, - multiple-statements, locally-disabled, too-many-arguments, useless-suppression, + duplicate-code, useless-object-inheritance, - too-many-ancestors, - ungrouped-imports, - assignment-from-no-return -# undefined-all-variable - + import-outside-toplevel, + self-assigning-variable, + consider-using-f-string, + consider-using-assignment-expr, + use-dict-literal, + missing-type-doc, + missing-param-doc, + differing-param-doc, + differing-type-doc, + compare-to-zero, + docstring-first-line-empty, + +enable=consider-using-augmented-assign [FORMAT] -# duplicated from setup.cfg -max-line-length=160 +max-line-length=100 +max-module-lines=1100 [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. #notes=FIXME,XXX,TODO -# Disable that, we don't want them in the report (???) +# Disable that, we don't want them to fail the lint CI job. notes= [VARIABLES] dummy-variables-rgx=_.* +init-import=true [TYPECHECK] # List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular +# system, and so shouldnt trigger E1101 when accessed. Python regular # expressions are accepted. -# gevent: this is helpful for py3/py2 code. -generated-members=exc_clear +generated-members=REQUEST,acl_users,aq_parent,providedBy + + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +# XXX: deprecated in 2.14; replaced with ignored-checks-for-mixins. +# The defaults for that value seem to be what we want +#ignore-mixin-members=yes # List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work +# (useful for classes with attributes dynamically set). This can work # with qualified names. -# greenlet, Greenlet, parent, dead: all attempts to fix issues in greenlet.py -# only seen on the service, e.g., self.parent.loop: class parent has no loop -# ignored-classes=SSLContext, SSLSocket, greenlet, Greenlet, parent, dead +#ignored-classes=SSLContext, SSLSocket, greenlet, Greenlet, parent, dead + # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. -# ignored-modules=gevent._corecffi +#ignored-modules=gevent._corecffi,gevent.os,os,greenlet,threading,gevent.libev.corecffi,gevent.socket,gevent.core,gevent.testing.support +ignored-modules=psycopg2.errors [DESIGN] max-attributes=12 +max-parents=10 +# Bump complexity up one. +max-complexity=11 [BASIC] -bad-functions=input # Prospector turns ot unsafe-load-any-extension by default, but # pylint leaves it off. This is the proximal cause of the # undefined-all-variable crash. unsafe-load-any-extension = yes +# This does not seem to work, hence the init-hook +property-classes=zope.cachedescriptors.property.Lazy,zope.cachedescriptors.property.Cached + +[CLASSES] +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. + + # Local Variables: # mode: conf diff --git a/CHANGES.rst b/CHANGES.rst index 1359906..8da859e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,10 +3,11 @@ ========= -0.0.2 (unreleased) +1.0.0 (unreleased) ================== -- Nothing changed yet. +- Drop support for Python < 3.10. +- Use native namespace packages. 0.0.1 (2020-01-02) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..96625b5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "wheel", + "setuptools", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a76f272..0000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[nosetests] -cover-package=nti.traversal - -[aliases] -dev = develop easy_install nti.traversal[test] - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index c20ad94..f99bebf 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ -import codecs -from setuptools import setup, find_packages + +from setuptools import setup +from setuptools import find_namespace_packages entry_points = { 'console_scripts': [ @@ -7,14 +8,14 @@ } TESTS_REQUIRE = [ - 'fudge', 'nti.testing', 'zope.testrunner', + 'coverage', ] def _read(fname): - with codecs.open(fname, encoding='utf-8') as f: + with open(fname, encoding='utf-8') as f: return f.read() @@ -37,25 +38,21 @@ def _read(fname): 'Natural Language :: English', 'Operating System :: OS Independent', 'Development Status :: 3 - Alpha', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], zip_safe=True, - packages=find_packages('src'), + packages=find_namespace_packages('src'), package_dir={'': 'src'}, include_package_data=True, - namespace_packages=['nti'], - tests_require=TESTS_REQUIRE, install_requires=[ - 'setuptools', 'repoze.lru', - 'six', 'zope.container', 'zope.component', 'zope.interface', @@ -71,4 +68,5 @@ def _read(fname): ] }, entry_points=entry_points, + python_requires=">=3.10", ) diff --git a/src/nti/__init__.py b/src/nti/__init__.py index 656dc0f..69e3be5 100644 --- a/src/nti/__init__.py +++ b/src/nti/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/src/nti/traversal/compat.py b/src/nti/traversal/compat.py index b62b034..92c0819 100644 --- a/src/nti/traversal/compat.py +++ b/src/nti/traversal/compat.py @@ -1,16 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import division -from __future__ import print_function -from __future__ import absolute_import -import six -from six.moves import urllib_parse from repoze.lru import lru_cache try: - from pyramid.traversal import _segment_cache + from pyramid.traversal import _segment_cache # pylint:disable=import-private-name except ImportError: # pragma: no cover _segment_cache = {} @@ -19,64 +14,39 @@ def url_quote(val, safe=''): # bw compat api + import urllib.parse cls = val.__class__ - if cls is six.text_type: + if cls is str: val = val.encode('utf-8') - elif cls is not six.binary_type: + elif cls is not bytes: val = str(val).encode('utf-8') # pylint: disable=redundant-keyword-arg - return urllib_parse.quote(val, safe=safe) - - -if six.PY2: - def native_(s, encoding='latin-1', errors='strict'): - """ - If ``s`` is an instance of ``text_type``, return - ``s.encode(encoding, errors)``, otherwise return ``str(s)`` - """ - if isinstance(s, six.text_type): - return s.encode(encoding, errors) - return str(s) -else: # pragma: no cover - def native_(s, encoding='latin-1', errors='strict'): - """ - If ``s`` is an instance of ``text_type``, return - ``s``, otherwise return ``str(s, encoding, errors)`` - """ - if isinstance(s, six.text_type): - return s - return str(s, encoding, errors) - - -if six.PY2: - def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): - """ - Return a quoted representation of a 'path segment' - """ - try: - return _segment_cache[(segment, safe)] - except KeyError: - if segment.__class__ is six.text_type: - result = url_quote(segment.encode('utf-8'), safe) - else: - result = url_quote(str(segment), safe) - _segment_cache[(segment, safe)] = result - return result -else: # pragma: no cover - def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): - """ - Return a quoted representation of a 'path segment' - """ - try: - return _segment_cache[(segment, safe)] - except KeyError: - if segment.__class__ not in (six.text_type, six.binary_type): - segment = str(segment) - result = url_quote(native_(segment, 'utf-8'), safe) - # we don't need a lock to mutate _segment_cache, as the below - # will generate exactly one Python bytecode (STORE_SUBSCR) - _segment_cache[(segment, safe)] = result - return result + return urllib.parse.quote(val, safe=safe) + + +def native_(s, encoding='latin-1', errors='strict'): + """ + If ``s`` is an instance of ``text_type``, return + ``s``, otherwise return ``str(s, encoding, errors)`` + """ + if isinstance(s, str): + return s + return str(s, encoding, errors) + +def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): + """ + Return a quoted representation of a 'path segment' + """ + try: + return _segment_cache[(segment, safe)] + except KeyError: + if segment.__class__ not in (str, bytes): + segment = str(segment) + result = url_quote(native_(segment, 'utf-8'), safe) + # we don't need a lock to mutate _segment_cache, as the below + # will generate exactly one Python bytecode (STORE_SUBSCR) + _segment_cache[(segment, safe)] = result + return result @lru_cache(1000) diff --git a/src/nti/traversal/tests/test_compat.py b/src/nti/traversal/tests/test_compat.py index 308965d..cc8d92c 100644 --- a/src/nti/traversal/tests/test_compat.py +++ b/src/nti/traversal/tests/test_compat.py @@ -26,7 +26,7 @@ def test_url_quote(self): assert_that(url_quote('ichigo and azien'), is_('ichigo%20and%20azien')) - assert_that(url_quote(u'ichigo and azien'), + assert_that(url_quote('ichigo and azien'), is_('ichigo%20and%20azien')) assert_that(url_quote(b'ichigo and azien'), @@ -40,10 +40,10 @@ def __str__(self): is_('ichigo%20and%20azien')) def test_native(self): - assert_that(native_(u'Ichigo', 'utf-8'), + assert_that(native_('Ichigo', 'utf-8'), is_('Ichigo')) assert_that(native_(b'Ichigo', 'utf-8'), - is_(u'Ichigo')) + is_('Ichigo')) def test_quote_path_segment(self): assert_that(quote_path_segment('aizen'), @@ -52,7 +52,7 @@ def test_quote_path_segment(self): assert_that(quote_path_segment(b'ichigo'), is_('ichigo')) - assert_that(quote_path_segment(u'ichigo'), + assert_that(quote_path_segment('ichigo'), is_('ichigo')) class Bleach(object): diff --git a/src/nti/traversal/tests/test_traversal.py b/src/nti/traversal/tests/test_traversal.py index ab327c4..b02229b 100644 --- a/src/nti/traversal/tests/test_traversal.py +++ b/src/nti/traversal/tests/test_traversal.py @@ -1,12 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import print_function, absolute_import, division __docformat__ = "restructuredtext en" # pylint: disable=protected-access,too-many-public-methods import unittest +from unittest.mock import patch as Patch from hamcrest import is_ from hamcrest import none @@ -15,9 +15,7 @@ from hamcrest import has_property from hamcrest import contains_string -import fudge -import zope.testing.loghandler from zope import interface @@ -54,12 +52,12 @@ class Root(object): @interface.implementer(ILocation) class Middle(object): __parent__ = Root() - __name__ = u'Middle' + __name__ = 'Middle' @interface.implementer(ILocation) class Leaf(object): __parent__ = Middle() - __name__ = u'\u2019' + __name__ = '\u2019' @interface.implementer(ILocation) class Invalid(object): @@ -82,19 +80,20 @@ class Invalid(object): resource_path(Invalid()) def test_traversal_no_root(self): - + from zope.testing.loggingsupport import InstalledHandler @interface.implementer(ILocation) class Middle(object): __parent__ = None - __name__ = u'Middle' + __name__ = 'Middle' @interface.implementer(ILocation) class Leaf(object): __parent__ = Middle() - __name__ = u'\u2019' + __name__ = '\u2019' + + log_handler = InstalledHandler('nti.traversal.traversal') + self.addCleanup(log_handler.uninstall) - log_handler = zope.testing.loghandler.Handler(None) - log_handler.add('nti.traversal.traversal') try: with self.assertRaises(TypeError): resource_path(Leaf()) @@ -105,19 +104,19 @@ class Leaf(object): finally: log_handler.close() - @fudge.patch('nti.traversal.traversal.path_adapter') + @Patch('nti.traversal.traversal.path_adapter', autospec=True) def test_adapter_traversable(self, mock_pa): - mock_pa.is_callable().with_args().returns(None) + mock_pa.return_value = None @interface.implementer(IContained) class Root(object): __parent__ = None - __name__ = u'Root' + __name__ = 'Root' @interface.implementer(ILocation) class Middle(object): __parent__ = Root() - __name__ = u'Middle' + __name__ = 'Middle' def get(self, key, default=None): if key == 'root': @@ -127,7 +126,7 @@ def get(self, key, default=None): @interface.implementer(IContained) class Leaf(object): __parent__ = Middle() - __name__ = u'Leaf' + __name__ = 'Leaf' mid = Middle() c = ContainerAdapterTraversable(mid) @@ -146,7 +145,7 @@ class Leaf(object): with self.assertRaises(LocationError): c.traverse('leaf', '') - mock_pa.is_callable().with_args().returns(Leaf()) + mock_pa.return_value = Leaf() assert_that(c.traverse('Leaf', ''), is_(Leaf)) @@ -162,12 +161,12 @@ def test_find_nearest_site(self): @interface.implementer(ILocation) class Middle(object): __parent__ = None - __name__ = u'Middle' + __name__ = 'Middle' @interface.implementer(ILocation) class Leaf(object): __parent__ = Middle() - __name__ = u'\u2019' + __name__ = '\u2019' assert_that(find_nearest_site(Leaf(), marker, ignore=ILocation), is_(marker)) @@ -182,7 +181,7 @@ class Context(object): assert_that(find_nearest_site(context, marker), is_(marker)) - @fudge.patch('nti.traversal.traversal.path_adapter') + @Patch('nti.traversal.traversal.path_adapter', autospec=True) def test_default_traversable(self, mock_pa): @interface.implementer(IContained) class Root(object): @@ -192,9 +191,9 @@ class Root(object): @interface.implementer(ILocation) class Middle(object): __parent__ = Root() - __name__ = u'Middle' + __name__ = 'Middle' - mock_pa.is_callable().with_args().returns(Root()) + mock_pa.return_value = Root() d = DefaultAdapterTraversable(Middle(), object()) assert_that(d.traverse('Root', ''), is_(Root)) diff --git a/src/nti/traversal/traversal.py b/src/nti/traversal/traversal.py index 88f1d67..01704c4 100644 --- a/src/nti/traversal/traversal.py +++ b/src/nti/traversal/traversal.py @@ -4,13 +4,8 @@ Generic traversal functions (and adapters). """ -from __future__ import division -from __future__ import print_function -from __future__ import absolute_import - import warnings - -import six +import logging from zope.interface import implementer from zope.component import queryMultiAdapter @@ -32,7 +27,7 @@ from nti.traversal.location import find_interface as _p_find_interface -logger = __import__('logging').getLogger(__name__) +logger = logging.getLogger(__name__) __all__ = [ 'resource_path', @@ -46,6 +41,7 @@ 'ContainerAdapterTraversable', 'DefaultAdapterTraversable', ] +# pylint:disable=assignment-from-no-return def resource_path(res): # This function is somewhat more flexible than Pyramid's, and @@ -109,9 +105,9 @@ def normal_resource_path(res): def is_valid_resource_path(target): # We really want to check if this is a valid HTTP URL path. How best to do that? # Not documented until we figure it out. - return isinstance(target, six.string_types) and (target.startswith('/') or - target.startswith('http://') or - target.startswith('https://')) + return isinstance(target, str) and (target.startswith('/') or + target.startswith('http://') or + target.startswith('https://')) def find_nearest_site(context, root=None, ignore=None): @@ -207,7 +203,7 @@ class adapter_request(adapter): """ def __init__(self, context, request=None): - super(adapter_request, self).__init__(context, request) + super().__init__(context, request) self.request = request def traverse(self, name, ignored): @@ -217,13 +213,13 @@ def traverse(self, name, ignored): if result is None: # Look for the single-adapter. Or raise location error - result = super(adapter_request, self).traverse(name, ignored) + result = super().traverse(name, ignored) # Some sanity checks on the returned object # pylint: disable=unused-variable __traceback_info__ = result, self.context, result.__parent__, result.__name__ - assert IContained.providedBy(result) + assert IContained.providedBy(result) # pylint:disable=no-value-for-parameter assert result.__parent__ is not None if result.__name__ is None: @@ -248,13 +244,13 @@ class ContainerAdapterTraversable(_ContainerTraversable): lambda self, nv: setattr(self, "_container", nv)) def __init__(self, context, request=None): - super(ContainerAdapterTraversable, self).__init__(context) + super().__init__(context) self.context = context self.request = request - def traverse(self, key, remaining_path): # pylint: disable=arguments-differ + def traverse(self, key, remaining_path): # pylint: disable=arguments-renamed try: - return super(ContainerAdapterTraversable, self).traverse(key, remaining_path) + return super().traverse(key, remaining_path) except KeyError: # Is there a named path adapter? adapted = adapter_request(self.context, self.request) @@ -273,13 +269,13 @@ class DefaultAdapterTraversable(_DefaultTraversable): """ def __init__(self, context, request=None): - super(DefaultAdapterTraversable, self).__init__(context) + super().__init__(context) self.context = context self.request = request - def traverse(self, key, remaining_path): # pylint: disable=arguments-differ + def traverse(self, key, remaining_path): # pylint: disable=arguments-renamed try: - return super(DefaultAdapterTraversable, self).traverse(key, remaining_path) + return super().traverse(key, remaining_path) except KeyError: # Is there a named path adapter? adapted = adapter_request(self.context, self.request) diff --git a/tox.ini b/tox.ini index cd282d3..45f98b8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,33 +1,33 @@ [tox] -envlist = - py27,py36,py37,py38,pypy,pypy3,coverage,docs - -setenv = - CHAMELEON_CACHE={envbindir} +envlist = pypy,py27,py36,py37,py38,py39,pypy3,coverage,docs [testenv] -usedevelop = true +# JAM: The comment and setting are cargo-culted from zope.interface. +# ``usedevelop`` is required otherwise unittest complains that it +# discovers a file in src/... but imports it from .tox/.../ +# ``skip_install`` also basically works, but that causes the ``extras`` +# not to be installed (though ``deps`` still are), and doesn't +# rebuild C extensions. + extras = test -deps = pylint commands = - pylint nti.traversal - python -m zope.testrunner --test-path=src + coverage run -p -m zope.testrunner --test-path=src --auto-color --auto-progress [] # substitute with tox positional args +setenv = + PYTHONHASHSEED=1042466059 + ZOPE_INTERFACE_STRICT_IRO=1 [testenv:coverage] -basepython = - python3.8 +# The -i/--ignore arg may be necessary, I'm not sure. +# It was cargo-culted over from zope.interface. commands = - coverage run -p -m zope.testrunner --test-path=src coverage combine - coverage report --fail-under=82 -deps = - coverage + coverage report -i + coverage html -i +depends = py27,py36,py37,py38,py39,pypy,pypy3,docs +parallel_show_output = true [testenv:docs] -basepython = - python3.8 +extras = docs commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html - sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest -deps = - .[docs] + sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctests