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..459bb5a --- /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 src + - 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 new file mode 100644 index 0000000..814d8a3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,220 @@ +[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] + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). +# NOTE: comments must go ABOVE the statement. In Python 2, mixing in +# comments disables all directives that follow, while in Python 3, putting +# comments at the end of the line does the same thing (though Py3 supports +# mixing) + + +# 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 +# exec-used, ; should tag individual instances with this, there are some but not too many +# 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 +# ; or landscape.io on Py3). The file causing the problem is unclear. UPDATE: identified and disabled +# that file. +# 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. +# 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, + too-few-public-methods, + global-statement, + locally-disabled, + too-many-arguments, + useless-suppression, + duplicate-code, + useless-object-inheritance, + 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] +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 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 shouldnt trigger E1101 when accessed. Python regular +# expressions are accepted. +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 can work +# with qualified names. +#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,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] +# 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 +# End: diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..17a626b --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,37 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Some things can only be configured on the RTD dashboard. +# Those that we may have changed from the default include: + +# Analytics code: +# Show Version Warning: False +# Single Version: True + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + builder: html + configuration: docs/conf.py + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Set the version of Python and requirements required to build your +# docs +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CHANGES.rst b/CHANGES.rst index 54b7e0e..2d54bed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,10 +3,12 @@ ========= -0.0.3 (unreleased) +1.0.0 (unreleased) ================== -- Nothing changed yet. +- Drop support for Python < 3.10. +- Add support for Python up to 3.13. +- Use native namespace packages. 0.0.2 (2020-06-19) diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index a8d7380..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,3 +0,0 @@ -@Library("nti.javascript-modules") _ -buildoutPipeline { -} diff --git a/MANIFEST.in b/MANIFEST.in index dd0fa08..4bc9ee5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,8 @@ include nose2.cfg include tox.ini include .travis.yml include *.txt +include *.yml +include .pylintrc exclude .nti_cover_package recursive-include docs *.py recursive-include docs *.rst diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index e05e286..0000000 --- a/babel.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[lingua_python: **.py] - -[lingua_xml: **.pt] - -[extractors] -lingua_xml = nti.utils.babel:extract_xml -lingua_python = nti.utils.babel:extract_python diff --git a/doc-requirements.txt b/doc-requirements.txt deleted file mode 100644 index e9704b8..0000000 --- a/doc-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -.[docs] diff --git a/docs/conf.py b/docs/conf.py index c3ed556..cc9804d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -172,25 +172,31 @@ ] - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'https://docs.python.org/': None, - 'https://persistent.readthedocs.io/en/latest': None, - 'https://acquisition.readthedocs.io/en/latest': None, - 'https://zcintid.readthedocs.io/en/latest': None, - 'https://zopeintid.readthedocs.io/en/latest': None, - 'https://zopelocation.readthedocs.io/en/latest': None, - 'https://btrees.readthedocs.io/en/latest': None, - 'https://zodb-docs.readthedocs.io/en/latest': None, +intersphinx_mapping = { + 'component': ('https://zopecomponent.readthedocs.io/en/latest/', None), + 'container': ('https://zopecontainer.readthedocs.io/en/latest/', None,), + 'i18n': ('https://zopei18nmessageid.readthedocs.io/en/latest/', None), + 'interface': ('https://zopeinterface.readthedocs.io/en/latest/', None), + 'persistent': ('https://persistent.readthedocs.io/en/latest', None), + 'python': ('https://docs.python.org/', None), + 'schema': ('https://zopeschema.readthedocs.io/en/latest/', None), + 'site': ('https://zopesite.readthedocs.io/en/latest/', None,), + 'testing': ('https://ntitesting.readthedocs.io/en/latest/', None), + 'traversing': ('https://zopetraversing.readthedocs.io/en/latest/', None), + 'zodb': ('http://www.zodb.org/en/latest/', None), + 'external': ('https://ntiexternalization.readthedocs.io/en/latest/', None), + 'acquisition': ('https://acquisition.readthedocs.io/en/latest', None,), + 'zcintid': ('https://zcintid.readthedocs.io/en/latest', None), + 'zopeintid': ('https://zopeintid.readthedocs.io/en/latest', None,), + 'location': ('https://zopelocation.readthedocs.io/en/latest', None,), + 'btrees': ('https://btrees.readthedocs.io/en/latest', None) } extlinks = { 'issue': ('https://github.com/NextThought/nti.intid/issues/%s', - 'issue #'), + 'issue #%s'), 'pr': ('https://github.com/NextThought/nti.intid/pull/%s', - 'pull request #')} + 'pull request #%s')} # Sphinx 1.8+ prefers this to `autodoc_default_flags`. It's documented that # either True or None mean the same thing as just setting the flag, but diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..73d2302 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "wheel", + "setuptools>=75.3", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0501e63..0000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[nosetests] -cover-package=nti.intid - -[aliases] -dev = develop easy_install nti.intid[test] - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index 9c96007..4872c4c 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import codecs from setuptools import setup -from setuptools import find_packages +from setuptools import find_namespace_packages entry_points = { 'console_scripts': [ @@ -10,7 +10,6 @@ TESTS_REQUIRE = [ 'coverage', 'ZODB', - 'fudge', 'nti.site', 'nti.testing', 'persistent', @@ -46,29 +45,26 @@ def _read(fname): 'Operating System :: OS Independent', 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: Apache Software License', - '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', ], url="https://github.com/OpenNTI/nti.intid", 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=[ 'Acquisition', 'BTrees', 'nti.externalization', 'nti.ntiids', 'nti.wref', - 'setuptools', 'zc.intid', 'zope.component', 'zope.deferredimport', @@ -89,4 +85,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/intid/__init__.py b/src/nti/intid/__init__.py index 60ccd1e..fb7b46f 100644 --- a/src/nti/intid/__init__.py +++ b/src/nti/intid/__init__.py @@ -1,13 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -.. $Id$ +Root of nti.intid. """ from __future__ import division from __future__ import print_function from __future__ import absolute_import +__all__ = [ + 'INTIIntIdAddedEvent', + 'INTIIntIdRemovedEvent', + 'NTIIntIdAddedEvent', + 'NTIIntIdRemovedEvent', + +] + from zc.intid.interfaces import IAfterIdAddedEvent as INTIIntIdAddedEvent from zc.intid.interfaces import IBeforeIdRemovedEvent as INTIIntIdRemovedEvent diff --git a/src/nti/intid/interfaces.py b/src/nti/intid/interfaces.py index e7b30d4..876f328 100644 --- a/src/nti/intid/interfaces.py +++ b/src/nti/intid/interfaces.py @@ -30,6 +30,7 @@ IntIdRemovedEvent='zc.intid.interfaces:BeforeIdRemovedEvent') # pylint:disable=no-method-argument,no-self-argument +# pylint:disable=too-many-ancestors class IIntIds(IZCIIntIds, IIntIdsSubclass, IContained): """ diff --git a/src/nti/intid/tests/__init__.py b/src/nti/intid/tests/__init__.py index a2a5052..697af4d 100644 --- a/src/nti/intid/tests/__init__.py +++ b/src/nti/intid/tests/__init__.py @@ -23,8 +23,10 @@ class mock_db_trans(site_testing.persistent_site_trans): def on_connection_opened(self, conn): - super(mock_db_trans, self).on_connection_opened(conn) from nti.intid.utility import IntIds + + super().on_connection_opened(conn) + folder = conn.root()[str(self.main_application_folder_name)] lsm = folder.getSiteManager() intids = IntIds('_ds_intid', family=BTrees.family64) diff --git a/src/nti/intid/tests/test_utility.py b/src/nti/intid/tests/test_utility.py index 617ff43..e523656 100644 --- a/src/nti/intid/tests/test_utility.py +++ b/src/nti/intid/tests/test_utility.py @@ -68,7 +68,7 @@ def register(self, *args, **kwargs): class TestUtility(AbstractTestBase): def setUp(self): - super(TestUtility, self).setUp() + super().setUp() eventtesting.setUp() def test_interface(self): @@ -92,7 +92,7 @@ def test_event(self): stub.add(obj) assert_that(stub.db(), is_(stub)) - assert_that(obj, + assert_that(obj, has_property('_p_jar', is_(stub))) count = 1 @@ -129,15 +129,15 @@ def test_force(self): obj._ds_id = 100 stub = ConnectionStub() stub.add(obj) - + u.force_register(100, obj) - assert_that(calling(u.force_register).with_args(100, P()), + assert_that(calling(u.force_register).with_args(100, P()), raises(IntIdInUseError)) - - assert_that(calling(u.force_unregister).with_args(200, obj), + + assert_that(calling(u.force_unregister).with_args(200, obj), raises(KeyError)) - assert_that(calling(u.force_unregister).with_args(100, P()), + assert_that(calling(u.force_unregister).with_args(100, P()), raises(KeyError)) u.force_unregister(100, obj, True)