diff --git a/.editorconfig b/.editorconfig index 824b9c3..ce35108 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ # https://github.com/zopefoundation/meta/tree/master/config/zope-product # # EditorConfig Configuration file, for more details see: -# http://EditorConfig.org +# https://EditorConfig.org # EditorConfig is a convention description, that could be interpreted # by multiple editors to enforce common coding conventions for specific # file types diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..cf37dd3 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,36 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/zope-product +name: pre-commit + +on: + pull_request: + push: + branches: + - master + # Allow to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + FORCE_COLOR: 1 + +jobs: + pre-commit: + permissions: + contents: read + pull-requests: write + name: linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd #v3.0.1 + with: + extra_args: --all-files --show-diff-on-failure + env: + PRE_COMMIT_COLOR: always + - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 #v1.1.0 + if: always() + with: + msg: Apply pre-commit code formatting diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dfbf3c2..dab1e65 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,55 +12,57 @@ on: jobs: build: + permissions: + contents: read + pull-requests: write strategy: # We want to see all failures: fail-fast: false matrix: os: - - ["ubuntu", "ubuntu-20.04"] + - ["ubuntu", "ubuntu-latest"] config: # [Python version, tox env] - - ["3.9", "release-check"] - - ["3.9", "lint"] - - ["3.7", "py37"] - - ["3.8", "py38"] - - ["3.9", "py39"] - - ["3.10", "py310"] - - ["3.11", "py311"] - - ["3.12", "py312"] - - ["3.9", "docs"] - - ["3.9", "coverage"] + - ["3.11", "release-check"] + - ["3.10", "py310"] + - ["3.11", "py311"] + - ["3.12", "py312"] + - ["3.13", "py313"] + - ["3.14", "py314"] + - ["3.11", "docs"] + - ["3.11", "coverage"] runs-on: ${{ matrix.os[1] }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name name: ${{ matrix.config[1] }} steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 with: - python-version: ${{ matrix.config[0] }} - - name: Pip cache - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.config[0] }}- - ${{ runner.os }}-pip- - - name: Install dependencies + persist-credentials: false + - name: Install additional dependencies run: | - python -m pip install --upgrade pip - pip install tox set -ex sudo apt update sudo apt install -y ldap-utils slapd libldap2-dev libsasl2-dev + - name: Install uv + caching + # astral/setup-uv@7.1.3 + uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 + with: + enable-cache: true + cache-dependency-glob: | + setup.* + tox.ini + python-version: ${{ matrix.config[0] }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Test - run: tox -e ${{ matrix.config[1] }} + if: ${{ !startsWith(runner.os, 'Mac') }} + run: uvx --with tox-uv tox -e ${{ matrix.config[1] }} + - name: Test (macOS) + if: ${{ startsWith(runner.os, 'Mac') }} + run: uvx --with tox-uv tox -e ${{ matrix.config[1] }}-universal2 - name: Coverage if: matrix.config[1] == 'coverage' run: | - pip install coveralls - coveralls --service=github + uvx coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.meta.toml b/.meta.toml index e6fb18c..46e6302 100644 --- a/.meta.toml +++ b/.meta.toml @@ -2,7 +2,7 @@ # https://github.com/zopefoundation/meta/tree/master/config/zope-product [meta] template = "zope-product" -commit-id = "acd8d239" +commit-id = "9fcd3d67" [python] with-windows = false @@ -26,6 +26,7 @@ additional-rules = [ [check-manifest] additional-ignores = [ "docs/_build/html/_static/*", + "docs/_build/html/_static/scripts/*", ] [coverage] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cdeaf6c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/zope-product +minimum_pre_commit_version: '3.6' +repos: + - repo: https://github.com/pycqa/isort + rev: "7.0.0" + hooks: + - id: isort + - repo: https://github.com/hhatto/autopep8 + rev: "v2.3.2" + hooks: + - id: autopep8 + args: [--in-place, --aggressive, --aggressive] + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.0 + hooks: + - id: pyupgrade + args: [--py310-plus] + - repo: https://github.com/isidentical/teyit + rev: 0.4.3 + hooks: + - id: teyit + - repo: https://github.com/PyCQA/flake8 + rev: "7.3.0" + hooks: + - id: flake8 + additional_dependencies: + - flake8-debugger == 4.1.2 diff --git a/CHANGES.rst b/CHANGES.rst index d62ae25..bf1e68a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,10 @@ releases, see the file `HISTORY.txt` in this folder. ---------------- +- Add support for Python 3.13, 3.14. + +- Drop support for Python 3.7, 3.8, 3.9. + 5.2 (2024-01-03) ---------------- - update to latest zope meta config templates diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da489e0..bbf4194 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,7 @@ + # Contributing to dataflake projects The projects under the dataflake GitHub organization are open source and diff --git a/MANIFEST.in b/MANIFEST.in index 3683513..9cdc393 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include *.rst include *.txt include buildout.cfg include tox.ini +include .pre-commit-config.yaml recursive-include docs *.py recursive-include docs *.rst diff --git a/buildout.cfg b/buildout.cfg index 01578d7..0c66508 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -1,6 +1,6 @@ [buildout] extends = - https://zopefoundation.github.io/Zope/releases/master/versions-prod.cfg + https://zopefoundation.github.io/Zope/releases/master/versions.cfg develop = . parts = test diff --git a/docs/conf.py b/docs/conf.py index ab3b289..f898e46 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,26 +5,25 @@ import datetime import os -import pkginfo import sys +from importlib.metadata import distribution + -parent = os.path.dirname(os.path.dirname(__file__)) -parent_dir = os.path.abspath(parent) -pkg_info = pkginfo.Develop(parent_dir) -pkg_version = pkg_info.version or '' year = datetime.datetime.now().year +sys.path.append(os.path.abspath('../src')) +rqmt = distribution('Products.LDAPUserFolder') # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'Products.LDAPUserFolder' -copyright = '2000-%i, Jens Vagelpohl and Contributors' % year +copyright = f'2000-{year}, Jens Vagelpohl and Contributors' author = 'Jens Vagelpohl' # The short X.Y version. -version = pkg_version.replace('.dev0', '') +version = '%s.%s' % tuple(map(int, rqmt.version.split('.')[:2])) # The full version, including alpha/beta/rc tags. -release = pkg_version +release = rqmt.version # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -37,5 +36,5 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' +html_theme = 'furo' html_static_path = ['_static'] diff --git a/docs/configuration.rst b/docs/configuration.rst index 9d6fe99..9c450d3 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -5,9 +5,6 @@ These are the configuration options sorted by the :term:`ZMI` navigation tab they appear on. -.. contents:: - :local: - :depth: 1 Configure: Basic configuration ------------------------------ diff --git a/docs/requirements.txt b/docs/requirements.txt index a4f8e86..4956bd5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,3 @@ -docutils<0.19 sphinx -sphinx_rtd_theme>1 -pkginfo +furo repoze.sphinx.autointerface diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3841a9d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/zope-product + +[build-system] +requires = [ + "setuptools >= 78.1.1,< 81", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.coverage.run] +branch = true +source = ["Products.LDAPUserFolder"] + +[tool.coverage.report] +fail_under = 81 +precision = 2 +ignore_errors = true +show_missing = true +exclude_lines = [ + "pragma: no cover", + "pragma: nocover", + "except ImportError:", + "raise NotImplementedError", + "if __name__ == '__main__':", + "self.fail", + "raise AssertionError", + "raise unittest.Skip", +] + +[tool.coverage.html] +directory = "parts/htmlcov" diff --git a/setup.cfg b/setup.cfg index bcf1c22..832c2c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,5 @@ # Generated from: # https://github.com/zopefoundation/meta/tree/master/config/zope-product -[bdist_wheel] -universal = 0 [flake8] doctests = 1 @@ -14,6 +12,7 @@ ignore = .meta.toml docs/_build/html/_sources/* docs/_build/html/_static/* + docs/_build/html/_static/scripts/* [isort] force_single_line = True diff --git a/setup.py b/setup.py index 07ed3bc..6204524 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ import os -from setuptools import find_packages from setuptools import setup @@ -25,11 +24,13 @@ def read(name): return fp.read() -setup(name='Products.LDAPUserFolder', - version='5.3.dev0', - description='A LDAP-enabled Zope user folder', - long_description=read('README.rst'), - classifiers=[ +setup( + name='Products.LDAPUserFolder', + version='5.3.dev0', + description='A LDAP-enabled Zope user folder', + long_description=read('README.rst'), + long_description_content_type='text/x-rst', + classifiers=[ "Development Status :: 6 - Mature", "Environment :: Web Environment", "Framework :: Zope", @@ -38,52 +39,42 @@ def read(name): "License :: OSI Approved :: Zope Public License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Internet :: WWW/HTTP :: Site Management", "Topic :: Software Development", "Topic :: System :: Systems Administration ::" - " Authentication/Directory :: LDAP", - ], - keywords='web application server zope authentication ldap', - author="Jens Vagelpohl and contributors", - author_email="jens@dataflake.org", - url="https://github.com/dataflake/Products.LDAPUserFolder", - project_urls={ + "Authentication/Directory :: LDAP", + ], + keywords='web application server zope authentication ldap', + author="Jens Vagelpohl and contributors", + author_email="jens@dataflake.org", + url="https://github.com/dataflake/Products.LDAPUserFolder", + project_urls={ 'Documentation': 'https://productsldapuserfolder.readthedocs.io/', - 'Issue Tracker': ('https://github.com/dataflake/' - 'Products.LDAPUserFolder/issues'), + 'Issue Tracker': ( + 'https://github.com/dataflake/' + 'Products.LDAPUserFolder/issues'), 'Sources': 'https://github.com/dataflake/Products.LDAPUserFolder', - }, - license="ZPL 2.1", - packages=find_packages('src'), - include_package_data=True, - namespace_packages=['Products'], - package_dir={'': 'src'}, - zip_safe=False, - python_requires='>=3.7', - install_requires=[ - 'setuptools', - 'Zope >= 5', - 'dataflake.cache', - 'dataflake.fakeldap', - 'python-ldap >= 3.3', - ], - extras_require={ + }, + license="ZPL-2.1", + python_requires='>=3.10', + install_requires=[ + 'Zope >= 5', + 'dataflake.cache', + 'dataflake.fakeldap', + 'python-ldap >= 3.3', + ], + extras_require={ 'exportimport': ['Products.GenericSetup >= 2.0b1'], 'docs': [ - 'Sphinx', - 'sphinx_rtd_theme', - 'repoze.sphinx.autointerface', - 'pkginfo'], - }, - entry_points=""" - [zope2.initialize] - Products.LDAPUserFolder = Products.LDAPUserFolder:initialize - """, - ) + 'Sphinx', + 'furo', + 'repoze.sphinx.autointerface', + ], + }, +) diff --git a/src/Products/LDAPUserFolder/LDAPUserFolder.py b/src/Products/LDAPUserFolder/LDAPUserFolder.py index e19cbf2..a28de1c 100644 --- a/src/Products/LDAPUserFolder/LDAPUserFolder.py +++ b/src/Products/LDAPUserFolder/LDAPUserFolder.py @@ -84,7 +84,7 @@ class LDAPUserFolder(BasicUserFolder): manage_options = ( ({'label': 'Configure', 'action': 'manage_main'}, - {'label': 'LDAP Servers', 'action': 'manage_servers'}, + {'label': 'LDAP Servers', 'action': 'manage_servers'}, {'label': 'LDAP Schema', 'action': 'manage_ldapschema'}, {'label': 'Caches', 'action': 'manage_cache'}, {'label': 'Users', 'action': 'manage_userrecords'}, @@ -222,7 +222,7 @@ def _lookupuserbyattr(self, name, value, pwd=None): bind_dn=bind_dn, bind_pwd=bind_pwd) if res['size'] == 0 or res['exception']: - msg = '_lookupuserbyattr: No user "%s=%s" (%s)' % ( + msg = '_lookupuserbyattr: No user "{}={}" ({})'.format( name, value, res['exception'] or 'n/a') logger.debug(msg) return None, None, None, None @@ -317,7 +317,7 @@ def manage_changeProperty(self, prop_name, prop_value, @security.protected(change_ldapuserfolder) def manage_edit(self, title, login_attr, uid_attr, users_base, - users_scope, roles, groups_base, groups_scope, + users_scope, roles, groups_base, groups_scope, binduid, bindpwd, binduid_usage=1, rdn_attr='cn', obj_classes='top,person', local_groups=0, implicit_mapping=0, encryption='SHA', read_only=0, @@ -653,7 +653,7 @@ def getUserByAttr(self, name, value, pwd=None, cache=0): else: login_name = login_name[0] elif len(login_name) == 0: - msg = 'getUserByAttr: "%s" has no "%s" (Login) value!' % ( + msg = 'getUserByAttr: "{}" has no "{}" (Login) value!'.format( user_dn, self._login_attr) logger.debug(msg) self._cache('negative').set(negative_cache_key, NonexistingUser()) @@ -662,8 +662,8 @@ def getUserByAttr(self, name, value, pwd=None, cache=0): if self._uid_attr != 'dn' and len(uid) > 0: uid = uid[0] elif len(uid) == 0: - msg = 'getUserByAttr: "%s" has no "%s" (UID Attribute) value!' % ( - user_dn, self._uid_attr) + msg = (f'getUserByAttr: "{user_dn}" has no' + f'"{self._uid_attr}" (UID Attribute) value!') logger.debug(msg) self._cache('negative').set(negative_cache_key, NonexistingUser()) return None @@ -1144,7 +1144,7 @@ def getGroupType(self, group_dn): scope=self._delegate.BASE, attrs=['objectClass']) if res['exception']: - msg = 'getGroupType: No group "%s" (%s)' % ( + msg = 'getGroupType: No group "{}" ({})'.format( group_dn, res['exception']) logger.info(msg) diff --git a/src/Products/LDAPUserFolder/exportimport.py b/src/Products/LDAPUserFolder/exportimport.py index 704e212..f519264 100644 --- a/src/Products/LDAPUserFolder/exportimport.py +++ b/src/Products/LDAPUserFolder/exportimport.py @@ -27,7 +27,7 @@ PROPERTIES = ('title', '_login_attr', '_uid_attr', 'users_base', - 'users_scope', '_roles', 'groups_base', 'groups_scope', + 'users_scope', '_roles', 'groups_base', 'groups_scope', '_binduid', '_bindpwd', '_binduid_usage', '_rdnattr', '_user_objclasses', '_local_groups', '_implicit_mapping', '_pwd_encryption', 'read_only', '_extra_user_filter', @@ -55,7 +55,7 @@ def _exportNode(self): node.appendChild(self._extractLDAPSchema()) self._logger.info('LDAPUserFolder at %s exported.' % ( - self.context.absolute_url_path())) + self.context.absolute_url_path())) return node def _importNode(self, node): @@ -72,7 +72,7 @@ def _importNode(self, node): self._initLDAPSchema(node) self._logger.info('LDAPUserFolder at %s imported.' % ( - self.context.absolute_url_path())) + self.context.absolute_url_path())) node = property(_exportNode, _importNode) diff --git a/src/Products/LDAPUserFolder/tests/test_LDAPUser.py b/src/Products/LDAPUserFolder/tests/test_LDAPUser.py index 2d9aeb3..752804f 100644 --- a/src/Products/LDAPUserFolder/tests/test_LDAPUser.py +++ b/src/Products/LDAPUserFolder/tests/test_LDAPUser.py @@ -60,25 +60,27 @@ def testLDAPUserInstantiation(self): ae(u.getId(), ug('cn')) ae(u.getUserName(), ug('mail')) for role in ug('user_roles'): - self.assertTrue(role in u.getRoles()) - self.assertTrue('Authenticated' in u.getRoles()) + self.assertIn(role, u.getRoles()) + self.assertIn('Authenticated', u.getRoles()) ae(u.getProperty('dn'), 'cn={},{}'.format(ug('cn'), dg('users_base'))) ae(u.getUserDN(), 'cn={},{}'.format(ug('cn'), dg('users_base'))) ae(u._getLDAPGroups(), tuple(ug('ldap_groups'))) - self.assertTrue(DateTime() >= u.getCreationTime()) + self.assertGreaterEqual(DateTime(), u.getCreationTime()) def testUnicodeAttributes(self): # Internally, most attributes are stored as str. # Test some to make sure. - self.assertTrue(isinstance(self.u_ob.id, str)) - self.assertTrue(isinstance(self.u_ob.name, str)) - self.assertTrue(isinstance(self.u_ob._properties['givenName'], str)) + self.assertIsInstance(self.u_ob.id, str) + self.assertIsInstance(self.u_ob.name, str) + self.assertIsInstance(self.u_ob._properties['givenName'], str) def testBinaryAttributes(self): # Some attributes are marked binary # These must not get encoded by _verifyUnicode - self.assertTrue( - self.u_ob._properties['jpegPhoto'] == self.image_contents) + self.assertEqual( + self.u_ob._properties['jpegPhoto'], + self.image_contents + ) def testMappedAttrs(self): ae = self.assertEqual @@ -93,10 +95,10 @@ def testMultivaluedAttributes(self): multivals = ug('multivalued_attrs') for mv in multivals: - self.assertTrue(isinstance(u.getProperty(mv), (list, tuple))) + self.assertIsInstance(u.getProperty(mv), (list, tuple)) def testNameUnicode(self): # Make sure name and ID are never bytes u = self.u_ob - self.assertFalse(isinstance(u.getUserName(), bytes)) - self.assertFalse(isinstance(u.getId(), bytes)) + self.assertNotIsInstance(u.getUserName(), bytes) + self.assertNotIsInstance(u.getId(), bytes) diff --git a/src/Products/LDAPUserFolder/tests/test_LDAPUserFolder.py b/src/Products/LDAPUserFolder/tests/test_LDAPUserFolder.py index 0cd7d81..0a909cf 100644 --- a/src/Products/LDAPUserFolder/tests/test_LDAPUserFolder.py +++ b/src/Products/LDAPUserFolder/tests/test_LDAPUserFolder.py @@ -206,13 +206,13 @@ def testAddUser(self): msg = acl.manage_addUser(REQUEST=None, kwargs=user) self.assertTrue(not msg) msg = acl.manage_addUser(REQUEST=None, kwargs=user) - self.assertTrue(msg.split(' ')[0] == 'ALREADY_EXISTS') + self.assertEqual(msg.split(' ')[0], 'ALREADY_EXISTS') user_ob = acl.getUser(ug(acl.getProperty('_login_attr'))) self.assertNotEqual(user_ob, None) for role in ug('user_roles'): - self.assertTrue(role in user_ob.getRoles()) + self.assertIn(role, user_ob.getRoles()) for role in acl.getProperty('_roles'): - self.assertTrue(role in user_ob.getRoles()) + self.assertIn(role, user_ob.getRoles()) ae(user_ob.getProperty('cn'), ug('cn')) ae(user_ob.getProperty('sn'), ug('sn')) ae(user_ob.getId(), ug(acl.getProperty('_uid_attr'))) @@ -558,12 +558,12 @@ def testEditUserRoles(self): self.assertTrue(not msg) user_ob = acl.getUser(ug(acl.getProperty('_login_attr'))) self.assertNotEqual(user_ob, None) - self.assertTrue(new_role not in user_ob.getRoles()) + self.assertNotIn(new_role, user_ob.getRoles()) user_dn = user_ob.getUserDN() acl.manage_editUserRoles(user_dn, ['Manager', new_role]) user_ob = acl.getUser(ug(acl.getProperty('_login_attr'))) self.assertNotEqual(user_ob, None) - self.assertTrue(new_role in user_ob.getRoles()) + self.assertIn(new_role, user_ob.getRoles()) def testEditUserRolesReadOnly(self): acl = self.folder.acl_users @@ -575,14 +575,14 @@ def testEditUserRolesReadOnly(self): self.assertTrue(not msg) user_ob = acl.getUser(ug(acl.getProperty('_login_attr'))) self.assertNotEqual(user_ob, None) - self.assertTrue(new_role not in user_ob.getRoles()) + self.assertNotIn(new_role, user_ob.getRoles()) user_dn = user_ob.getUserDN() acl._delegate.read_only = 1 acl.manage_editUserPassword(user_dn, 'newpass') acl.manage_editUserRoles(user_dn, ['Manager', new_role]) user_ob = acl.getUser(ug(acl.getProperty('_login_attr'))) self.assertNotEqual(user_ob, None) - self.assertTrue(new_role not in user_ob.getRoles()) + self.assertNotIn(new_role, user_ob.getRoles()) def testModRDN(self): acl = self.folder.acl_users @@ -607,9 +607,9 @@ def testModRDN(self): new_dn = 'cn=new,%s' % acl.getProperty('users_base') ae(user_ob.getUserDN(), new_dn) for role in ug('user_roles'): - self.assertTrue(role in user_ob.getRoles()) + self.assertIn(role, user_ob.getRoles()) for role in acl.getProperty('_roles'): - self.assertTrue(role in user_ob.getRoles()) + self.assertIn(role, user_ob.getRoles()) noSecurityManager() def testSetUserProperty(self): @@ -702,7 +702,7 @@ def testGetAttributesOfAllObjects(self): search_string, attributes) for attr in attributes: - self.assertTrue(attr in res) + self.assertIn(attr, res) def testNegativeCaching(self): ae = self.assertEqual @@ -754,18 +754,22 @@ def testGetUserFilterString(self): acl = self.folder.acl_users filt_string = acl._getUserFilterString() for ob_class in acl.getProperty('_user_objclasses'): - self.assertTrue('(objectclass=%s)' % ob_class.lower() - in filt_string.lower()) - self.assertTrue('(%s=*)' % dg('uid_attr') in filt_string.lower()) + self.assertIn( + '(objectclass=%s)' % ob_class.lower(), + filt_string.lower() + ) + self.assertIn('(%s=*)' % dg('uid_attr'), filt_string.lower()) filters = ['(uid=test)', '(cn=test)'] filt_string = acl._getUserFilterString(filters=filters) for ob_class in acl.getProperty('_user_objclasses'): - self.assertTrue('(objectclass=%s)' % ob_class.lower() - in filt_string.lower()) + self.assertIn( + '(objectclass=%s)' % ob_class.lower(), + filt_string.lower() + ) for filt in filters: - self.assertTrue(filt in filt_string) - self.assertFalse('(%s=*)' % dg('uid_attr') in filt_string.lower()) + self.assertIn(filt, filt_string) + self.assertNotIn('(%s=*)' % dg('uid_attr'), filt_string.lower()) # Set up some different values acl.manage_edit(title=ag('title'), login_attr=ag('login_attr'), @@ -784,19 +788,23 @@ def testGetUserFilterString(self): filt_string = acl._getUserFilterString() for ob_class in acl.getProperty('_user_objclasses'): - self.assertTrue('(objectclass=%s)' % ob_class.lower() - in filt_string.lower()) - self.assertTrue(ag('extra_user_filter') in filt_string) - self.assertTrue('(%s=*)' % ag('uid_attr') in filt_string) + self.assertIn( + '(objectclass=%s)' % ob_class.lower(), + filt_string.lower() + ) + self.assertIn(ag('extra_user_filter'), filt_string) + self.assertIn('(%s=*)' % ag('uid_attr'), filt_string) filters = ['(uid=test)', '(cn=test)'] filt_string = acl._getUserFilterString(filters=filters) for ob_class in acl.getProperty('_user_objclasses'): - self.assertTrue('(objectclass=%s)' % ob_class.lower() - in filt_string.lower()) + self.assertIn( + '(objectclass=%s)' % ob_class.lower(), + filt_string.lower() + ) for filt in filters: - self.assertTrue(filt in filt_string) - self.assertFalse('(%s=*)' % ag('uid_attr') in filt_string) + self.assertIn(filt, filt_string) + self.assertNotIn('(%s=*)' % ag('uid_attr'), filt_string) def test_expireUser(self): # http://www.dataflake.org/tracker/issue_00617 etc. @@ -839,4 +847,4 @@ def test_manage_reinit(self): self.assertFalse(acl._cache('authenticated').get('user1')) self.assertFalse(acl._cache('anonymous').get('user1')) self.assertFalse(acl._cache('negative').get('user1')) - self.assertFalse(acl._hash == old_hash) + self.assertNotEqual(acl._hash, old_hash) diff --git a/src/Products/LDAPUserFolder/tests/test_exportimport.py b/src/Products/LDAPUserFolder/tests/test_exportimport.py index 5df1cab..1a73376 100644 --- a/src/Products/LDAPUserFolder/tests/test_exportimport.py +++ b/src/Products/LDAPUserFolder/tests/test_exportimport.py @@ -170,13 +170,13 @@ def test_normal(self): 'conn_timeout': 10, 'op_timeout': 10} svr2 = {'host': '/var/spool/ldapi', 'port': 0, 'protocol': 'ldapi', 'conn_timeout': 2, 'op_timeout': 2} - self.assertTrue(svr1 in servers) - self.assertTrue(svr2 in servers) + self.assertIn(svr1, servers) + self.assertIn(svr2, servers) local_groups = list(acl._groups_store.items()) self.assertEqual(len(local_groups), 2) - self.assertTrue(('user1', ['posixAdmin', 'foobar']) in local_groups) - self.assertTrue(('user2', ['baz']) in local_groups) + self.assertIn(('user1', ['posixAdmin', 'foobar']), local_groups) + self.assertIn(('user2', ['baz']), local_groups) def test_servers_purge(self): from Products.GenericSetup.tests.common import DummyImportContext @@ -196,8 +196,8 @@ def test_servers_purge(self): 'conn_timeout': 1, 'op_timeout': 1} svr2 = {'host': '/tmp/ldapi', 'port': 0, 'protocol': 'ldapi', 'conn_timeout': 20, 'op_timeout': 20} - self.assertTrue(svr1 in servers) - self.assertTrue(svr2 in servers) + self.assertIn(svr1, servers) + self.assertIn(svr2, servers) def test_servers_nopurge(self): from Products.GenericSetup.tests.common import DummyImportContext @@ -221,10 +221,10 @@ def test_servers_nopurge(self): 'conn_timeout': 10, 'op_timeout': 10} svr4 = {'host': '/var/spool/ldapi', 'port': 0, 'protocol': 'ldapi', 'conn_timeout': 2, 'op_timeout': 2} - self.assertTrue(svr1 in servers) - self.assertTrue(svr2 in servers) - self.assertTrue(svr3 in servers) - self.assertTrue(svr4 in servers) + self.assertIn(svr1, servers) + self.assertIn(svr2, servers) + self.assertIn(svr3, servers) + self.assertIn(svr4, servers) def test_schema_purge(self): from Products.GenericSetup.tests.common import DummyImportContext diff --git a/src/Products/LDAPUserFolder/tests/test_group_role.py b/src/Products/LDAPUserFolder/tests/test_group_role.py index becbb06..8d22d43 100644 --- a/src/Products/LDAPUserFolder/tests/test_group_role.py +++ b/src/Products/LDAPUserFolder/tests/test_group_role.py @@ -41,7 +41,7 @@ def test_implicitRoleMapping(self): mapped_roles = acl._mapRoles(have_roles) self.assertEqual(len(mapped_roles), 2) for role in have_roles: - self.assertTrue(role in mapped_roles) + self.assertIn(role, mapped_roles) acl.manage_edit(title=gp('title'), login_attr=gp('login_attr'), uid_attr=gp('uid_attr'), users_base=gp('users_base'), users_scope=gp('users_scope'), roles=gp('roles'), @@ -63,7 +63,7 @@ def test_groupMapping(self): self.assertEqual(len(acl.getGroupMappings()), 1) roles = acl._mapRoles(have_roles) self.assertEqual(len(roles), 1) - self.assertTrue('Manager' in roles) + self.assertIn('Manager', roles) acl.manage_deleteGroupMappings('unknown') self.assertEqual(len(acl.getGroupMappings()), 1) acl.manage_deleteGroupMappings(['ldap_group']) @@ -92,10 +92,10 @@ def test_searchGroups(self): # now let's check these groups work u = acl.getUser('test2') - self.assertFalse('Manager' in u.getRoles()) + self.assertNotIn('Manager', u.getRoles()) acl.manage_addGroupMapping(group_cn, 'Manager') u = acl.getUser('test2') - self.assertFalse('Manager' not in u.getRoles()) + self.assertIn('Manager', u.getRoles()) # ok, so now we can try group searches by attributes # Search on a bogus attribute, must return error result @@ -142,8 +142,8 @@ def test_groupLifecycle_nonutf8(self): all_groups = acl.getGroups() # Only one group record should exist, the one we just entered - self.assertTrue(len(all_groups) == 1) - self.assertTrue(all_groups[0][0] == groupid) + self.assertEqual(len(all_groups), 1) + self.assertEqual(all_groups[0][0], groupid) # Now delete the group. The DN we get back from getGroups will have # been recoded into whatever is set in utils.py (normally latin-1). @@ -151,7 +151,7 @@ def test_groupLifecycle_nonutf8(self): # deletion would fail silently and the group would still exist. group_dn = all_groups[0][1] acl.manage_deleteGroups(dns=[group_dn]) - self.assertTrue(len(acl.getGroups()) == 0) + self.assertEqual(len(acl.getGroups()), 0) def test_groupsWithCharactersNeedingEscaping(self): # http://www.dataflake.org/tracker/issue_00507 @@ -165,8 +165,8 @@ def test_groupsWithCharactersNeedingEscaping(self): all_groups = acl.getGroups() # Only one group record should exist, the one we just entered - self.assertTrue(len(all_groups) == 1) - self.assertTrue(all_groups[0][0] == groupid) + self.assertEqual(len(all_groups), 1) + self.assertEqual(all_groups[0][0], groupid) # Now delete the group. group_dn = all_groups[0][1] @@ -176,4 +176,4 @@ def test_groupsWithCharactersNeedingEscaping(self): # That means we cannot use the returned DN, we must construct it anew. group_dn = f'cn={groupid},{acl.groups_base}' acl.manage_deleteGroups(dns=[group_dn]) - self.assertTrue(len(acl.getGroups()) == 0) + self.assertEqual(len(acl.getGroups()), 0) diff --git a/src/Products/LDAPUserFolder/tests/test_schemamanagement.py b/src/Products/LDAPUserFolder/tests/test_schemamanagement.py index 2fa5175..fca1adb 100644 --- a/src/Products/LDAPUserFolder/tests/test_schemamanagement.py +++ b/src/Products/LDAPUserFolder/tests/test_schemamanagement.py @@ -27,7 +27,7 @@ def test_schema(self): self.assertEqual(len(acl.getLDAPSchema()), 3) self.assertEqual(len(acl.getSchemaDict()), 3) cur_schema = acl.getSchemaConfig() - self.assertTrue('mail' in cur_schema) + self.assertIn('mail', cur_schema) acl.manage_addLDAPSchemaItem('cn', 'exists', '', 'exists') self.assertEqual(len(acl.getLDAPSchema()), 3) self.assertEqual(len(acl.getSchemaDict()), 3) @@ -35,8 +35,8 @@ def test_schema(self): self.assertEqual(len(acl.getLDAPSchema()), 1) self.assertEqual(len(acl.getSchemaDict()), 1) cur_schema = acl.getSchemaConfig() - self.assertFalse('mail' in cur_schema) - self.assertFalse('cn' in cur_schema) + self.assertNotIn('mail', cur_schema) + self.assertNotIn('cn', cur_schema) def test_mapped_attributes(self): acl = self.folder.acl_users diff --git a/src/Products/__init__.py b/src/Products/__init__.py deleted file mode 100644 index de40ea7..0000000 --- a/src/Products/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/tox.ini b/tox.ini index 12ddfd7..d86805f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,32 +5,41 @@ minversion = 3.18 envlist = release-check lint - py37 - py38 - py39 py310 py311 py312 + py313 + py314 docs coverage [testenv] skip_install = true deps = - zc.buildout >= 3.0.1 - wheel > 0.37 + setuptools >= 78.1.1,< 81 + zc.buildout + wheel setenv = - py312: VIRTUALENV_PIP=23.1.2 - py312: PIP_REQUIRE_VIRTUALENV=0 commands_pre = {envbindir}/buildout -nc {toxinidir}/buildout.cfg buildout:directory={envdir} buildout:develop={toxinidir} install test commands = {envbindir}/test {posargs:-cv} + +[testenv:setuptools-latest] +basepython = python3 +deps = + git+https://github.com/pypa/setuptools.git\#egg=setuptools + zc.buildout + wheel + + [testenv:release-check] description = ensure that the distribution is ready to release basepython = python3 skip_install = true deps = + setuptools >= 78.1.1,< 81 + wheel twine build check-manifest @@ -39,34 +48,19 @@ deps = commands_pre = commands = check-manifest - check-python-versions + check-python-versions --only pyproject.toml,setup.py,tox.ini,.github/workflows/tests.yml python -m build --sdist --no-isolation twine check dist/* [testenv:lint] +description = This env runs all linters configured in .pre-commit-config.yaml basepython = python3 -commands_pre = - mkdir -p {toxinidir}/parts/flake8 -allowlist_externals = - mkdir -commands = - isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py - flake8 {toxinidir}/src {toxinidir}/setup.py +skip_install = true deps = - flake8 - isort - # Useful flake8 plugins that are Python and Plone specific: - flake8-coding - flake8-debugger - mccabe - -[testenv:isort-apply] -basepython = python3 + pre-commit commands_pre = -deps = - isort commands = - isort {toxinidir}/src {toxinidir}/setup.py [] + pre-commit run --all-files --show-diff-on-failure [testenv:docs] basepython = python3 @@ -85,27 +79,9 @@ allowlist_externals = mkdir deps = {[testenv]deps} - coverage + coverage[toml] commands = mkdir -p {toxinidir}/parts/htmlcov coverage run {envbindir}/test {posargs:-cv} coverage html - coverage report -m --fail-under=81 - -[coverage:run] -branch = True -source = Products.LDAPUserFolder - -[coverage:report] -precision = 2 -exclude_lines = - pragma: no cover - pragma: nocover - except ImportError: - raise NotImplementedError - if __name__ == '__main__': - self.fail - raise AssertionError - -[coverage:html] -directory = parts/htmlcov + coverage report