diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..c9fd1db --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + pull_request: + push: + branches: [master] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v2.4.0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2.3.2 + with: + python-version: ${{ matrix.python-version }} + + - name: Bootstrap poetry + shell: bash + run: curl -sSL https://install.python-poetry.org | python3 - + + - name: Configure poetry + shell: bash + run: poetry config virtualenvs.in-project true + + - name: Install dependencies + shell: bash + run: poetry install + + - name: Run pytest + shell: bash + run: poetry run python -m phabricator.tests.test_phabricator diff --git a/.gitignore b/.gitignore index f832a2b..97b2815 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build/ dist/ .idea +poetry.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ca2bed1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -python: -- '2.7' -- '3.5' -install: pip install .[tests] -script: python -m phabricator.tests.test_phabricator -deploy: - provider: pypi - user: disqus - password: - secure: AJ7zSLd6BgI4W8Kp3KEx5O40bUJA91PkgLTZb5MnCx4/8nUPlkk+LqvodaiiJQEGzpP8COPvRlzJ/swd8d0P38+Se6V83wA43MylimzrgngO6t3c/lXa/aMnrRzSpSfK5QznEMP2zcSU1ReD+2dNr7ATKajbGqTzrCFk/hQPMZ0= - on: - tags: true diff --git a/phabricator/__init__.py b/phabricator/__init__.py index 9277efa..75f6807 100644 --- a/phabricator/__init__.py +++ b/phabricator/__init__.py @@ -23,13 +23,10 @@ import socket import pkgutil import time +from typing import MutableMapping import requests from requests.adapters import HTTPAdapter, Retry -from ._compat import ( - MutableMapping, iteritems, string_types, urlencode, -) - __all__ = ['Phabricator'] @@ -71,7 +68,7 @@ ARCRC = {} for conf in ARC_CONFIGS: if os.path.exists(conf): - with open(conf, 'r') as fobj: + with open(conf) as fobj: ARCRC.update(json.load(fobj)) @@ -100,11 +97,11 @@ 'pair': tuple, # str types - 'str': string_types, - 'string': string_types, - 'phid': string_types, - 'guids': string_types, - 'type': string_types, + 'str': str, + 'string': str, + 'phid': str, + 'guids': str, + 'type': str, } TYPE_INFO_COMMENT_RE = re.compile(r'\s*\([^)]+\)\s*$') @@ -133,9 +130,9 @@ def map_param_type(param_type): if sub_match: sub_type = sub_match.group(1).lower() - return [PARAM_TYPE_MAP.setdefault(sub_type, string_types)] + return [PARAM_TYPE_MAP.setdefault(sub_type, str)] - return PARAM_TYPE_MAP.setdefault(main_type, string_types) + return PARAM_TYPE_MAP.setdefault(main_type, str) def parse_interfaces(interfaces): @@ -146,7 +143,7 @@ def parse_interfaces(interfaces): """ parsed_interfaces = collections.defaultdict(dict) - for m, d in iteritems(interfaces): + for m, d in interfaces.items(): app, func = m.split('.', 1) method = parsed_interfaces[app][func] = {} @@ -158,7 +155,7 @@ def parse_interfaces(interfaces): method['optional'] = {} method['required'] = {} - for name, type_info in iteritems(dict(d['params'])): + for name, type_info in dict(d['params']).items(): # Set the defaults optionality = 'required' param_type = 'string' @@ -194,7 +191,7 @@ def __init__(self, code, message): self.message = message def __str__(self): - return '%s: %s' % (self.code, self.message) + return f'{self.code}: {self.message}' class Result(MutableMapping): @@ -219,10 +216,10 @@ def __len__(self): return len(self.response) def __repr__(self): - return '<%s: %s>' % (type(self).__name__, repr(self.response)) + return f'<{type(self).__name__}: {repr(self.response)}>' -class Resource(object): +class Resource: def __init__(self, api, interface=None, endpoint=None, method=None, nested=False): self.api = api self._interface = interface or copy.deepcopy(parse_interfaces(INTERFACES)) @@ -244,7 +241,7 @@ def __getattr__(self, attr): return getattr(self, attr) interface = self._interface if self.nested: - attr = "%s.%s" % (self.endpoint, attr) + attr = f"{self.endpoint}.{attr}" submethod_exists = False submethod_match = attr + '.' for key in interface.keys(): @@ -283,8 +280,8 @@ def validate_kwarg(key, target): raise ValueError('Wrong argument type: %s is not a list' % key) elif not validate_kwarg(kwargs.get(key), val): if isinstance(val, list): - raise ValueError('Wrong argument type: %s is not a list of %ss' % (key, val[0])) - raise ValueError('Wrong argument type: %s is not a %s' % (key, val)) + raise ValueError(f'Wrong argument type: {key} is not a list of {val[0]}s') + raise ValueError(f'Wrong argument type: {key} is not a {val}') conduit = self.api._conduit @@ -313,13 +310,13 @@ def validate_kwarg(key, target): } # TODO: Use HTTP "method" from interfaces.json - path = '%s%s.%s' % (self.api.host, self.method, self.endpoint) + path = f'{self.api.host}{self.method}.{self.endpoint}' response = self.session.post(path, data=body, headers=headers, timeout=self.api.timeout) # Make sure we got a 2xx response indicating success if not response.status_code >= 200 or not response.status_code < 300: raise requests.exceptions.HTTPError( - 'Bad response status: {0}'.format(response.status_code) + f'Bad response status: {response.status_code}' ) data = self._parse_response(response.text) @@ -366,7 +363,7 @@ def __init__(self, username=None, certificate=None, host=None, self.clientDescription = socket.gethostname() + ':python-phabricator' self._conduit = None - super(Phabricator, self).__init__(self, **kwargs) + super().__init__(self, **kwargs) def _request(self, **kwargs): raise SyntaxError('You cannot call the Conduit API without a resource.') diff --git a/phabricator/_compat.py b/phabricator/_compat.py deleted file mode 100644 index c87f3a8..0000000 --- a/phabricator/_compat.py +++ /dev/null @@ -1,26 +0,0 @@ -import sys - -PY3 = sys.version_info[0] >= 3 - -try: - from collections.abc import MutableMapping -except ImportError: - from collections import MutableMapping - -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode - -if PY3: - str_type = str - string_types = str, - - def iteritems(d, **kw): - return iter(d.items(**kw)) -else: - str_type = unicode - string_types = basestring, - - def iteritems(d, **kw): - return d.iteritems(**kw) diff --git a/phabricator/tests/test_phabricator.py b/phabricator/tests/test_phabricator.py index e12b285..dc70f63 100644 --- a/phabricator/tests/test_phabricator.py +++ b/phabricator/tests/test_phabricator.py @@ -167,7 +167,7 @@ def test_endpoint_shadowing(self): self.assertEqual( shadowed_endpoints, [], - "The following endpoints are shadowed: {}".format(shadowed_endpoints) + f"The following endpoints are shadowed: {shadowed_endpoints}" ) if __name__ == '__main__': diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7114ad7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[tool.poetry] +name = "phabricator" +version = "0.10.0" +authors = ["Disqus "] +repository = "http://github.com/disqus/python-phabricator" +description = "Phabricator API Bindings" +classifiers=[ + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Topic :: Software Development', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', +] + +[tool.poetry.dependencies] +python = "^3.7" + +[tool.poetry.dev-dependencies] +responses = "^0.18.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 3ec4f1a..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -import sys - -from setuptools import setup, find_packages - -tests_requires = ['responses>=0.12'] - -if sys.version_info[:2] < (2, 7): - tests_requires.append('unittest2') - -if sys.version_info[:2] <= (3, 3): - tests_requires.append('mock') - -setup( - name='phabricator', - version='0.9.1', - author='Disqus', - author_email='opensource@disqus.com', - url='http://github.com/disqus/python-phabricator', - description='Phabricator API Bindings', - packages=find_packages(), - zip_safe=False, - install_requires=['requests>=2.26'], - test_suite='phabricator.tests.test_phabricator', - extras_require={ - 'tests': tests_requires, - }, - include_package_data=True, - classifiers=[ - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Topic :: Software Development', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - ], -)