diff --git a/.coveragerc b/.coveragerc index 083fd44..a85767a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,15 @@ [run] source = nti.ntiids +# New in 5.0; required for the GHA coveralls submission. +relative_files = True +omit = + */flycheck_*py + */benchmarks/*py [report] exclude_lines = pragma: no cover raise NotImplementedError + raise AssertionError + Python 2 if __name__ == .__main__.: 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..6306898 --- /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..11bc75d --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,34 @@ +# .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 requirements required to build your +# docs + +build: + # os is required for some reason + os: ubuntu-22.04 + tools: + python: "3.11" + +python: + install: + - method: pip + path: . + extra_requirements: + - docs 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 9966dd2..7a3b12a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -173,17 +173,26 @@ # 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://zopeinterface.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), } extlinks = { 'issue': ('https://github.com/NextThought/nti.ntiids/issues/%s', - 'issue #'), + 'issue #%s'), 'pr': ('https://github.com/NextThought/nti.ntiids/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..16df773 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +build-backend = "setuptools.build_meta" +# Build dependencies. Remember to change these in make-manylinux and appveyor.yml +# if you add/remove/change them. +requires = [ + "setuptools >= 40.8.0", + "wheel", + + # Python 3.7 requires at least Cython 0.27.3. + # 0.28 is faster, and (important!) lets us specify the target module + # name to be created so that we can have both foo.py and _foo.so + # at the same time. 0.29 fixes some issues with Python 3.7, + # and adds the 3str mode for transition to Python 3. 0.29.14+ is + # required for Python 3.8. 3.0a2 introduced a change that prevented + # us from compiling (https://github.com/gevent/gevent/issues/1599) + # but once that was fixed, 3.0a4 led to all of our leak tests + # failing in Python 2 (https://travis-ci.org/github/gevent/gevent/jobs/683782800); + # This was fixed in 3.0a5 (https://github.com/cython/cython/issues/3578) + # 3.0a6 fixes an issue cythonizing source on 32-bit platforms + "Cython >= 3.0.11", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dbf34aa..0000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[nosetests] -cover-package=nti.ntiids - -[aliases] -dev = develop easy_install nti.ntiids[test] - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index a7d991c..31183b3 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,7 +8,6 @@ } TESTS_REQUIRE = [ - 'fudge', 'nti.testing', 'zope.dottedname', 'zope.testrunner', @@ -35,27 +35,24 @@ def _read(fname): 'Operating System :: OS Independent', 'Framework :: Zope3', 'Development Status :: 5 - Production/Stable', - '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/NextThought/nti.ntiids", 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=[ 'nti.externalization', 'nti.schema', 'repoze.lru', - 'setuptools', 'zope.component', 'zope.hookable', 'zope.i18nmessageid', @@ -72,4 +69,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/ntiids/common.py b/src/nti/ntiids/common.py index e647e07..b65e295 100644 --- a/src/nti/ntiids/common.py +++ b/src/nti/ntiids/common.py @@ -14,19 +14,17 @@ from nti.ntiids.ntiids import make_specific_safe #: NTI provider -NTI = u'NTI' +NTI = 'NTI' #: System username SYSTEM_USER_NAME = getattr(system_user, 'title').lower() -logger = __import__('logging').getLogger(__name__) - def generate_ntiid(nttype, provider=NTI, now=None): now = datetime.utcnow() if now is None else now dstr = now.strftime("%Y%m%d%H%M%S %f") rand = str(uuid.uuid4().time_low) - specific = make_specific_safe(u"%s_%s_%s" % (SYSTEM_USER_NAME, dstr, rand)) + specific = make_specific_safe("%s_%s_%s" % (SYSTEM_USER_NAME, dstr, rand)) result = make_ntiid(provider=provider, nttype=nttype, specific=specific) diff --git a/src/nti/ntiids/interfaces.py b/src/nti/ntiids/interfaces.py index 113da39..5c7d90e 100644 --- a/src/nti/ntiids/interfaces.py +++ b/src/nti/ntiids/interfaces.py @@ -8,7 +8,8 @@ from __future__ import print_function from __future__ import absolute_import -# pylint: disable=inherit-non-class, no-self-argument, no-method-argument +# pylint:disable=inherit-non-class, no-self-argument, no-method-argument +# pylint:disable=too-many-ancestors from zope import interface @@ -30,13 +31,13 @@ class INTIID(ITuple, IMinimalSequence): Note that NTIID components are text (unicode), and ntiid strings are also unicode. """ - provider = TextLine(title=u"The username of the creating/providing entity.") + provider = TextLine(title="The username of the creating/providing entity.") - nttype = TextLine(title=u"The type of the NTIID.") + nttype = TextLine(title="The type of the NTIID.") - specific = TextLine(title=u"The type-specific portion of the NTIID.") + specific = TextLine(title="The type-specific portion of the NTIID.") - date = TextLine(title=u"The date portion of the NTIID.") + date = TextLine(title="The date portion of the NTIID.") class INTIIDResolver(interface.Interface): diff --git a/src/nti/ntiids/ntiids.py b/src/nti/ntiids/ntiids.py index b21771d..149ed49 100644 --- a/src/nti/ntiids/ntiids.py +++ b/src/nti/ntiids/ntiids.py @@ -24,9 +24,9 @@ from zope import component from zope import interface +import zope.i18nmessageid from zope.schema.interfaces import ValidationError -from nti.ntiids import MessageFactory as _ from nti.ntiids.interfaces import INTIID from nti.ntiids.interfaces import INTIIDResolver @@ -35,31 +35,33 @@ from nti.ntiids._compat import text_ from nti.ntiids._compat import bytes_ +MessageFactory = zope.i18nmessageid.MessageFactory('nti.ntiids') +_ = MessageFactory # Well-known IDs -DATE = u"2011-10" +DATE = "2011-10" #: prefix of NTIIDs -TAG_NTC = u'tag:nextthought.com' +TAG_NTC = 'tag:nextthought.com' #: When NTIIDs (usually of a particular type) are arranged into #: a tree, or a forest of trees, this NTIID specifies the conceptual #: root of the entire tree or forest. -ROOT = u"%s,%s:Root" % (TAG_NTC, DATE) +ROOT = "%s,%s:Root" % (TAG_NTC, DATE) #: Used as an opaque identifier to a specific object. This will #: not incorporate the object's name or path (when those concepts make #: sense). Instead, it points to an object by identity. -TYPE_OID = u'OID' +TYPE_OID = 'OID' #: Meant to specify some sort of unique but otherwise #: meaningless local portion. (Not necessarily an actual GUID). -TYPE_UUID = u'UUID' +TYPE_UUID = 'UUID' #: The intid type is not currently used. Instead, #: intid is included as a part of the OID, preventing #: access using a stale URL after an object is deleted -TYPE_INTID = u'INTID' +TYPE_INTID = 'INTID' #: Used as an opaque identifier to refer to an object that was #: once (weakly) referenced, but can no longer be found. Only the system @@ -69,35 +71,35 @@ #: the same missing NTIID; however, in some cases, it may be possible #: for references to different missing objects to produce the same missing #: NTIID. Context will usually make it clear if this has happened. -TYPE_MISSING = u'Missing' +TYPE_MISSING = 'Missing' #: Named entities are globally accessible knowing nothing #: more than a simple string name. There should be a defined #: subtype for each namespace and/or specific kind of #: named entity -TYPE_NAMED_ENTITY = u'NamedEntity' +TYPE_NAMED_ENTITY = 'NamedEntity' #: Subtype of named entities identifying a particular user account -TYPE_NAMED_ENTITY_USER = TYPE_NAMED_ENTITY + u':User' +TYPE_NAMED_ENTITY_USER = TYPE_NAMED_ENTITY + ':User' #: Subtype of named entities identifying a particular community -TYPE_NAMED_ENTITY_COMMUNITY = TYPE_NAMED_ENTITY + u':Community' +TYPE_NAMED_ENTITY_COMMUNITY = TYPE_NAMED_ENTITY + ':Community' #: AKA an extant "chat" session -TYPE_ROOM = u'MeetingRoom' +TYPE_ROOM = 'MeetingRoom' TYPE_MEETINGROOM = TYPE_ROOM -TYPE_HTML = u'HTML' -TYPE_QUIZ = u'Quiz' +TYPE_HTML = 'HTML' +TYPE_QUIZ = 'Quiz' -TYPE_MEETINGROOM_GROUP = TYPE_ROOM + u':Group' +TYPE_MEETINGROOM_GROUP = TYPE_ROOM + ':Group' #: Transcripts and TranscriptSummaries. Note that #: they are not subtypes of a common type because they #: contain quite different information and are used #: in different ways. -TYPE_TRANSCRIPT = u'Transcript' -TYPE_TRANSCRIPT_SUMMARY = u'TranscriptSummary' +TYPE_TRANSCRIPT = 'Transcript' +TYPE_TRANSCRIPT_SUMMARY = 'TranscriptSummary' # Validation # This is a minimal set, required to make parsing wark; @@ -122,7 +124,7 @@ class ImpossibleToMakeSpecificPartSafe(InvalidNTIIDError): The supplied value cannot be used safely. """ - i18n_message = _(u"The value you have used is not valid.") + i18n_message = _("The value you have used is not valid.") ImpossibleToMakeProviderPartSafe = ImpossibleToMakeSpecificPartSafe @@ -137,21 +139,21 @@ def validate_ntiid_string(ntiid, encoding='utf-8'): try: # cannot decode unicode ntiid = ntiid if isinstance(ntiid, text_type) else ntiid.decode(encoding) - except (AttributeError, TypeError): - raise InvalidNTIIDError("Not a string " + repr(ntiid)) - except UnicodeDecodeError: + except (AttributeError, TypeError) as e: + raise InvalidNTIIDError("Not a string " + repr(ntiid)) from e + except UnicodeDecodeError as e: raise InvalidNTIIDError("String contains non-utf-8 values " + - repr(ntiid)) + repr(ntiid)) from e if not ntiid or not ntiid.startswith('tag:nextthought.com,20'): raise InvalidNTIIDError('Missing correct start value: ' + repr(ntiid)) # Split twice. Allow for : in the specific part - parts = ntiid.split(u':', 2) + parts = ntiid.split(':', 2) if len(parts) != 3: raise InvalidNTIIDError('Wrong number of colons: ' + ntiid) - if len(parts[2].split(u'-')) > 3: + if len(parts[2].split('-')) > 3: raise InvalidNTIIDError('Wrong number of dashes: ' + ntiid) for char in _illegal_chars_: @@ -198,7 +200,7 @@ def is_ntiid_of_types(ntiid, nttypes): the_type = get_type(ntiid) if the_type: # strip subtypes - the_type = the_type.split(u':', 2)[0] + the_type = the_type.split(':', 2)[0] if the_type in nttypes: return the_type return None @@ -214,7 +216,7 @@ def escape_provider(provider): :return: The escaped provider. """ - return text_(provider).replace(u' ', u'_').replace(u'-', u'_') + return text_(provider).replace(' ', '_').replace('-', '_') def make_provider_safe(provider): @@ -224,7 +226,7 @@ def make_provider_safe(provider): .. caution:: This is not a reversible transformation. """ - provider = re.sub(_illegal_chars_pattern, u'_', text_(provider)) + provider = re.sub(_illegal_chars_pattern, '_', text_(provider)) provider = escape_provider(provider) return provider @@ -364,18 +366,18 @@ def make_ntiid(date=DATE, provider=None, nttype=None, specific=None, base=None): elif isinstance(provider, text_type): # Strip high bytes provider = provider.encode('ascii', 'ignore').decode('ascii') - provider = escape_provider(provider) + u'-' + provider = escape_provider(provider) + '-' else: - provider = (base_parts.provider + u'-' if base_parts.provider else u'') + provider = (base_parts.provider + '-' if base_parts.provider else '') if specific: - specific = u'-' + text_(specific) + specific = '-' + text_(specific) else: - specific = (u'-' + base_parts.specific if base_parts.specific else u'') + specific = ('-' + base_parts.specific if base_parts.specific else '') nttype = nttype or base_parts.nttype __traceback_info__ = (date_string, provider, nttype, specific) - result = u'tag:nextthought.com,%s:%s%s%s' % __traceback_info__ + result = 'tag:nextthought.com,%s:%s%s%s' % __traceback_info__ return validate_ntiid_string(result) NTIID = collections.namedtuple('NTIID', @@ -474,7 +476,7 @@ def hexdigest(data, salt=None): def hash_ntiid(ntiid, salt=None): parts = get_parts(ntiid) digest = hexdigest(ntiid, salt).upper() - specific = make_specific_safe(u"%s_%04d" % (digest, len(ntiid))) + specific = make_specific_safe("%s_%04d" % (digest, len(ntiid))) ntiid = make_ntiid(parts.date, parts.provider, parts.nttype, diff --git a/src/nti/ntiids/oids.py b/src/nti/ntiids/oids.py index db12b5a..34b96cb 100644 --- a/src/nti/ntiids/oids.py +++ b/src/nti/ntiids/oids.py @@ -5,10 +5,6 @@ """ -from __future__ import division -from __future__ import print_function -from __future__ import absolute_import - from six import string_types from zope.security.management import system_user @@ -61,7 +57,7 @@ def to_external_ntiid_oid(contained, default_oid=None, :keyword bool mask_creator: If true (not the default), then the actual creator of the object will not be present in the NTIID string. """ - # pylint: disable=unused-variable + # pylint: disable=unused-variable,too-many-positional-arguments __traceback_info__ = type(contained) if callable(getattr(contained, 'to_external_ntiid_oid', None)): diff --git a/src/nti/ntiids/schema.py b/src/nti/ntiids/schema.py index d05c87d..f866d54 100644 --- a/src/nti/ntiids/schema.py +++ b/src/nti/ntiids/schema.py @@ -14,6 +14,7 @@ from nti.schema.field import ValidURI +# pylint:disable=too-many-ancestors class ValidNTIID(ValidURI): """ @@ -33,5 +34,5 @@ def fromUnicode(self, value): return value def _validate(self, value): - super(ValidNTIID, self)._validate(value) + super()._validate(value) validate_ntiid_string(value) diff --git a/src/nti/ntiids/tests/test_common.py b/src/nti/ntiids/tests/test_common.py index 95e76c1..46b4111 100644 --- a/src/nti/ntiids/tests/test_common.py +++ b/src/nti/ntiids/tests/test_common.py @@ -23,6 +23,6 @@ class TestCommon(unittest.TestCase): layer = SharedConfiguringTestLayer def test_generate_ntiid(self): - ntiid = generate_ntiid(u'FOO', now=datetime.utcfromtimestamp(1000)) + ntiid = generate_ntiid('FOO', now=datetime.utcfromtimestamp(1000)) assert_that(ntiid, starts_with('tag:nextthought.com,2011-10:NTI-FOO-system_19700101001640_000000')) diff --git a/src/nti/ntiids/tests/test_compat.py b/src/nti/ntiids/tests/test_compat.py index 2fa247f..3449143 100644 --- a/src/nti/ntiids/tests/test_compat.py +++ b/src/nti/ntiids/tests/test_compat.py @@ -21,5 +21,5 @@ class TestCompat(unittest.TestCase): def test_bytes(self): - assert_that(bytes_(u'\u2019'), is_(b'\xe2\x80\x99')) - assert_that(text_(b'\xe2\x80\x99'), is_(u'\u2019')) + assert_that(bytes_('\u2019'), is_(b'\xe2\x80\x99')) + assert_that(text_(b'\xe2\x80\x99'), is_('\u2019')) diff --git a/src/nti/ntiids/tests/test_ntiids.py b/src/nti/ntiids/tests/test_ntiids.py index 0535765..b46cfeb 100644 --- a/src/nti/ntiids/tests/test_ntiids.py +++ b/src/nti/ntiids/tests/test_ntiids.py @@ -55,55 +55,55 @@ class TestNTIIDS(TestCase): layer = SharedConfiguringTestLayer def test_make_ntiid_bytes_provider(self): - ntiid = make_ntiid(provider=b'abc', nttype=u'TYPE') + ntiid = make_ntiid(provider=b'abc', nttype='TYPE') self.assertEqual(ntiid, 'tag:nextthought.com,2011-10:abc-TYPE') def test_make_ntiid(self): self.assertRaises(ValueError, make_ntiid) self.assertRaises(ValueError, make_ntiid, - base=u'invalid') + base='invalid') self.assertRaises(ValueError, make_ntiid, - provider=u'foo', - specific=u'baz') + provider='foo', + specific='baz') self.assertRaises(ValueError, make_ntiid, date='', - nttype=u'Test', - specific=u'baz') + nttype='Test', + specific='baz') iso_now = datetime.date(*time.gmtime()[:3]).isoformat() - assert_that(make_ntiid(date=None, nttype=u'Test'), + assert_that(make_ntiid(date=None, nttype='Test'), is_('tag:nextthought.com,%s:Test' % iso_now)) - assert_that(make_ntiid(date=0, nttype=u'Test'), + assert_that(make_ntiid(date=0, nttype='Test'), is_('tag:nextthought.com,%s:Test' % iso_now)) - assert_that(make_ntiid(date=None, nttype=u'Test', provider=u'TestP'), + assert_that(make_ntiid(date=None, nttype='Test', provider='TestP'), is_('tag:nextthought.com,%s:TestP-Test' % iso_now)) - assert_that(make_ntiid(date=None, nttype=u'Test', provider=u'TestP', specific=u'Bar'), + assert_that(make_ntiid(date=None, nttype='Test', provider='TestP', specific='Bar'), is_('tag:nextthought.com,%s:TestP-Test-Bar' % iso_now)) - assert_that(make_ntiid(date=None, nttype=u'Test', provider=u'TestP', specific=b'0000'), + assert_that(make_ntiid(date=None, nttype='Test', provider='TestP', specific=b'0000'), is_('tag:nextthought.com,%s:TestP-Test-0000' % iso_now)) assert_that( make_ntiid(date=None, - nttype=u'Test', - provider=u'Henry Beach Needham . McClure\u2019s Magazine', - specific=u'Bar'), + nttype='Test', + provider='Henry Beach Needham . McClure\u2019s Magazine', + specific='Bar'), is_('tag:nextthought.com,%s' ':Henry_Beach_Needham_._McClures_Magazine-Test-Bar' % iso_now)) - assert_that(make_ntiid(base=u'tag:nextthought.com,2011-10:NTI-HTML-foo', - nttype=u'XML'), + assert_that(make_ntiid(base='tag:nextthought.com,2011-10:NTI-HTML-foo', + nttype='XML'), is_('tag:nextthought.com,2011-10:NTI-XML-foo')) - assert_that(make_ntiid(base=u'tag:nextthought.com,2011-10:NTI-HTML-foo', - nttype=u'XML', + assert_that(make_ntiid(base='tag:nextthought.com,2011-10:NTI-HTML-foo', + nttype='XML', provider=1,), is_('tag:nextthought.com,2011-10:1-XML-foo')) @@ -111,41 +111,41 @@ def test_parse_ntiid(self): ntiid = get_parts(ROOT) assert_that(ntiid, verifiably_provides(INTIID)) - ntiid = get_parts(u'mystrįng') + ntiid = get_parts('mystrįng') assert_that(ntiid, has_property('provider', is_(none()))) assert_that(ntiid, has_property('nttype', is_(none()))) assert_that(ntiid, has_property('specific', is_(none()))) assert_that(ntiid, has_property('date', is_(none()))) - ntiid = u'tag:nextthought.com,2011-10:Foo-Bar-With:Many:Colons' - validate_ntiid_string(ntiid) + ntiid_str = 'tag:nextthought.com,2011-10:Foo-Bar-With:Many:Colons' + validate_ntiid_string(ntiid_str) - ntiid = get_parts(ntiid) + ntiid = get_parts(ntiid_str) assert_that(ntiid, has_property('provider', 'Foo')) assert_that(ntiid, has_property('nttype', 'Bar')) assert_that(ntiid, has_property('specific', 'With:Many:Colons')) - ntiid = get_parts(u'tag:nextthought.com,20:Foo-Bar') + ntiid = get_parts('tag:nextthought.com,20:Foo-Bar') assert_that(ntiid, has_property('provider', is_(none()))) assert_that(ntiid, has_property('nttype', 'Foo')) assert_that(ntiid, has_property('specific', 'Bar')) assert_that(ntiid, has_property('date', '20')) with self.assertRaises(InvalidNTIIDError): - validate_ntiid_string(u'mystrįng') + validate_ntiid_string('mystrįng') if six.PY2: with self.assertRaises(InvalidNTIIDError): validate_ntiid_string('いちご', 'ascii') with self.assertRaises(InvalidNTIIDError): - validate_ntiid_string(u'tag:nextthought.com,20') + validate_ntiid_string('tag:nextthought.com,20') with self.assertRaises(InvalidNTIIDError): - validate_ntiid_string(u'tag:nextthought.com,20:NTI-HTML-764-85-31-19910') + validate_ntiid_string('tag:nextthought.com,20:NTI-HTML-764-85-31-19910') with self.assertRaises(InvalidNTIIDError): - validate_ntiid_string(u'tag:nextthought.com,20:NTI-HTML-????') + validate_ntiid_string('tag:nextthought.com,20:NTI-HTML-????') def test_is_valid_ntiid_string(self): assert_that(is_valid_ntiid_string(None), is_false()) @@ -161,17 +161,17 @@ def test_get_specific(self): def test_utc_date(self): #"A timestamp should always be interpreted UTC." # This date is 2012-01-05 in UTC, but 2012-01-04 in CST - assert_that(make_ntiid(date=1325723859.140755, nttype=u'Test'), + assert_that(make_ntiid(date=1325723859.140755, nttype='Test'), is_('tag:nextthought.com,2012-01-05:Test')) def test_make_safe(self): - assert_that(make_specific_safe(u'-Foo%Bar +baz:?'), + assert_that(make_specific_safe('-Foo%Bar +baz:?'), is_('_Foo_Bar__baz__')) - assert_that(make_specific_safe(u'-Foo%Bar/+baz:?'), + assert_that(make_specific_safe('-Foo%Bar/+baz:?'), is_('_Foo_Bar__baz__')) # lax lets more through - assert_that(make_specific_safe(u'-Foo%Bar, +baz:?', strict=False), + assert_that(make_specific_safe('-Foo%Bar, +baz:?', strict=False), is_('_Foo_Bar,_+baz:_')) # too short @@ -181,17 +181,17 @@ def test_make_safe(self): assert_that(calling(make_specific_safe).with_args(' '), raises(ImpossibleToMakeSpecificPartSafe)) - assert_that(calling(make_specific_safe).with_args(u'Алибра школа'), + assert_that(calling(make_specific_safe).with_args('Алибра школа'), raises(ImpossibleToMakeSpecificPartSafe)) - assert_that(make_provider_safe(u'NSF/[Science]Nation?'), + assert_that(make_provider_safe('NSF/[Science]Nation?'), is_('NSF__Science_Nation_')) assert_that(make_provider_safe(b'NSF/[Science]Nation?'), is_('NSF__Science_Nation_')) def test_hash_ntiid(self): - ntiid = u'tag:nextthought.com,2011-10:NTI-HTML-764853119912700730' + ntiid = 'tag:nextthought.com,2011-10:NTI-HTML-764853119912700730' assert_that( hash_ntiid(ntiid, '0000'), is_('tag:nextthought.com,2011-10:' @@ -199,13 +199,13 @@ def test_hash_ntiid(self): def test_find_object_with_ntiid(self): assert_that(find_object_with_ntiid(None), is_(none())) - assert_that(find_object_with_ntiid(u'invalid'), is_(none())) + assert_that(find_object_with_ntiid('invalid'), is_(none())) - ntiid = u'tag:nextthought.com,2011-10:NTI-HTML-764853119912700730' + ntiid = 'tag:nextthought.com,2011-10:NTI-HTML-764853119912700730' obj = find_object_with_ntiid(ntiid, error=True) assert_that(obj, is_(none())) - ntiid = u'tag:nextthought.com,2011-10:NTI-UUID-764853119912700730' + ntiid = 'tag:nextthought.com,2011-10:NTI-UUID-764853119912700730' obj = find_object_with_ntiid(ntiid, error=True) assert_that(obj, is_(none())) @@ -217,7 +217,7 @@ def resolve(self, unused_key): resolver = Resolver() component.getGlobalSiteManager().registerUtility(resolver, INTIIDResolver, 'HTML') try: - ntiid = u'tag:nextthought.com,2011-10:NTI-HTML-764853119912700730' + ntiid = 'tag:nextthought.com,2011-10:NTI-HTML-764853119912700730' obj = find_object_with_ntiid(ntiid) assert_that(obj, is_not(none())) finally: diff --git a/src/nti/ntiids/tests/test_oids.py b/src/nti/ntiids/tests/test_oids.py index ba9581f..2f9e65b 100644 --- a/src/nti/ntiids/tests/test_oids.py +++ b/src/nti/ntiids/tests/test_oids.py @@ -13,8 +13,8 @@ from hamcrest import has_entries from hamcrest import has_property -import fudge import unittest +from unittest.mock import patch as Patch from nti.externalization.extension_points import set_external_identifiers @@ -32,12 +32,13 @@ def test_hookable(self): assert_that(set_external_identifiers, has_property('implementation', is_(setExternalIdentifiers))) - @fudge.patch('nti.ntiids.oids.toExternalOID') + @Patch('nti.ntiids.oids.toExternalOID', autospec=True) def test_to_external_ntiid_oid(self, mock_teo): - mock_teo.is_callable().with_args().returns('0x01:666f6f') + mock_teo.return_value = '0x01:666f6f' ntiid = to_external_ntiid_oid(object()) - assert_that(ntiid, - is_('tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f')) + assert_that( + ntiid, + is_('tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f')) class O(object): creator = 'aizen' @@ -60,31 +61,32 @@ class C(object): assert_that(ntiid, is_('tag:nextthought.com,2011-10:ichigo-OID-0x01:666f6f')) - mock_teo.is_callable().with_args().returns(None) + mock_teo.return_value = None ntiid = to_external_ntiid_oid(object()) assert_that(ntiid, is_(none())) - @fudge.patch('nti.ntiids.oids.toExternalOID') + @Patch('nti.ntiids.oids.toExternalOID', autospec=True) def test_set_external_identifiers(self, mock_teo): - mock_teo.is_callable().with_args().returns('0x01:666f6f') + mock_teo.return_value = '0x01:666f6f' class Context(object): id = 'my-id' context = Context() result = dict() setExternalIdentifiers(context, result) - assert_that(result, - has_entries('NTIID', 'tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f', - 'OID', 'tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f', - 'ID', 'my-id')) + assert_that(result, has_entries( + NTIID=('tag:nextthought.com,' + '2011-10:zope.security.management.system_user-OID-0x01:666f6f'), + OID='tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f', + ID='my-id')) ntiid = 'tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f' class Note(object): id = ntiid - context = Note() + context = Note() # pylint:disable=redefined-variable-type result = dict(ID=ntiid) - mock_teo.is_callable().with_args().returns('0x01:666f6f') + mock_teo.return_value = '0x01:666f6f' setExternalIdentifiers(context, result) assert_that(result, has_entries('NTIID', ntiid, @@ -101,9 +103,10 @@ def to_external_ntiid_oid(self): is_('tag:nextthought.com,2011-10:system_user-OID-0x02:666f6f')) def test_volatile(self): + expected = 'tag:nextthought.com,2011-10:system_user-OID-0x03:777f6f' class O(object): - _v_to_external_ntiid_oid_False = 'tag:nextthought.com,2011-10:system_user-OID-0x03:777f6f' + _v_to_external_ntiid_oid_False = expected ntiid = to_external_ntiid_oid(O()) assert_that(ntiid, - is_('tag:nextthought.com,2011-10:system_user-OID-0x03:777f6f')) + is_(expected)) diff --git a/src/nti/ntiids/tests/test_schema.py b/src/nti/ntiids/tests/test_schema.py index 3fc6ad3..316e087 100644 --- a/src/nti/ntiids/tests/test_schema.py +++ b/src/nti/ntiids/tests/test_schema.py @@ -21,7 +21,8 @@ class TestSchema(unittest.TestCase): layer = SharedConfiguringTestLayer def test_schema(self): + nti = 'tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f' schema = ValidNTIID() - schema.fromUnicode(u'tag:nextthought.com,2011-10:zope.security.management.system_user-OID-0x01:666f6f') + schema.fromUnicode(nti) with self.assertRaises(InvalidURI): - schema.fromUnicode(u'xx') + schema.fromUnicode('xx')