pax_global_header00006660000000000000000000000064141347441740014523gustar00rootroot0000000000000052 comment=78687dd422cb77d365a36e5d047d0c71a11065d0 logfury-1.0.1/000077500000000000000000000000001413474417400132115ustar00rootroot00000000000000logfury-1.0.1/.github/000077500000000000000000000000001413474417400145515ustar00rootroot00000000000000logfury-1.0.1/.github/workflows/000077500000000000000000000000001413474417400166065ustar00rootroot00000000000000logfury-1.0.1/.github/workflows/cd.yml000066400000000000000000000027471413474417400177310ustar00rootroot00000000000000name: Continuous Delivery on: push: tags: 'v*' # push events to matching v*, i.e. v1.0, v20.15.10 env: PYTHON_DEFAULT_VERSION: "3.10" ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }} PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies run: python -m pip install --upgrade nox pip setuptools wheel - name: Build the distribution id: build run: nox -vs build - name: Read the Changelog id: read-changelog uses: mindsers/changelog-reader-action@v2 with: version: ${{ steps.build.outputs.version }} - name: Create GitHub release and upload the distribution uses: softprops/action-gh-release@v1 with: name: ${{ steps.build.outputs.version }} body: ${{ steps.read-changelog.outputs.changes }} draft: ${{ env.ACTIONS_STEP_DEBUG == 'true' }} prerelease: false files: ${{ steps.build.outputs.asset_path }} - name: Upload the distribution to PyPI if: ${{ env.PYPI_PASSWORD != '' }} uses: pypa/gh-action-pypi-publish@v1.3.1 with: user: __token__ password: ${{ env.PYPI_PASSWORD }} logfury-1.0.1/.github/workflows/ci.yml000066400000000000000000000040531413474417400177260ustar00rootroot00000000000000name: Continuous Integration on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: env: PYTHON_DEFAULT_VERSION: "3.10" jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies run: python -m pip install --upgrade nox pip setuptools - name: Run linters run: nox -vs lint - name: Validate changelog if: ${{ ! startsWith(github.ref, 'refs/heads/dependabot/') }} uses: zattoo/changelog@v1 with: token: ${{ github.token }} build: needs: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }} uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON_DEFAULT_VERSION }} - name: Install dependencies run: python -m pip install --upgrade nox pip setuptools wheel - name: Build the distribution run: nox -vs build test: needs: lint runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7"] exclude: - os: "macos-latest" python-version: "pypy-3.7" - os: "windows-latest" python-version: "pypy-3.7" steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: python -m pip install --upgrade nox pip setuptools - name: Run tests run: nox -vs test logfury-1.0.1/.gitignore000066400000000000000000000002011413474417400151720ustar00rootroot00000000000000*.pyc .codacy-coverage/ .coverage .eggs/ .idea .nox/ .python-version logfury.egg-info build coverage.xml dist venv .venv .vscode logfury-1.0.1/CHANGELOG.md000066400000000000000000000016031413474417400150220ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [1.0.1] - 2021-10-23 ### Fixed * Fixed decorating classes * Fixed arguments ordering in logs ## [1.0.0] - 2021-10-19 ### Added * ApiVer `v1` * Support for Python >3.6 up to 3.10 * Nox task manager * GitHub Actions CI * GitHub Actions CD ### Fixed * Handling `staticmethod` and `classmethod` * Support for decorating `class` * Logging traced object names ### Deprecated * Python <3.5 support [Unreleased]: https://github.com/Backblaze/b2-sdk-python/compare/v1.0.1...HEAD [1.0.1]: https://github.com/Backblaze/b2-sdk-python/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/Backblaze/b2-sdk-python/compare/0.1.2...v1.0.0 logfury-1.0.1/LICENSE000066400000000000000000000027361413474417400142260ustar00rootroot00000000000000BSD 3-clause license Copyright (c) 2016, Pawel Polewicz. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of logfury nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. logfury-1.0.1/MANIFEST.in000066400000000000000000000000511413474417400147430ustar00rootroot00000000000000include requirements.txt include LICENSE logfury-1.0.1/README.rst000066400000000000000000000107761413474417400147130ustar00rootroot00000000000000.. image:: https://img.shields.io/pypi/wheel/logfury.svg :target: https://pypi.python.org/pypi/logfury/ .. image:: https://img.shields.io/pypi/l/logfury.svg :target: https://pypi.python.org/pypi/logfury/ .. image:: https://img.shields.io/pypi/v/logfury.svg :target: https://pypi.python.org/pypi/logfury/ .. image:: https://img.shields.io/pypi/dm/logfury.svg :target: https://pypi.python.org/pypi/logfury/ .. image:: https://github.com/reef-technologies/logfury/workflows/CI/badge.svg?branch=master :target: https://github.com/reef-technologies/logfury/actions/workflows/ci.yml ======== Logfury ======== Logfury is a tool for python library maintainers. It allows for responsible, low-boilerplate logging of method calls. ***************************** whats with the weird import ***************************** .. sourcecode:: python from logfury.v1 import DefaultTraceMeta If you were to use logfury in your library, any change to the API could potentially break your program. Nobody wants that. Thanks to this import trick I can keep the API very stable. At the same time I can change the functionality of the library and change default behavior of next middle version, without changing the name of the package. This way YOU decide when to adopt potentially incompatible API changes, by incrementing the API version on import. This concept is called "apiver". ***************** Installation ***************** ^^^^^^^^^^^^^^^^^^^^ Current stable ^^^^^^^^^^^^^^^^^^^^ :: pip install logfury ^^^^^^^^^^^^^^^^^^^^ Development version ^^^^^^^^^^^^^^^^^^^^ :: git clone git@github.com:reef-technologies/logfury.git pip install -e . ***************** Basic usage ***************** ^^^^^^^^^^^^^^^^^^^^^^^^^^^ DefaultTraceMeta metaclass ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. sourcecode:: pycon >>> import logging >>> import six >>> >>> from logfury.v1 import DefaultTraceMeta, limit_trace_arguments, disable_trace >>> >>> >>> logging.basicConfig() >>> logger = logging.getLogger(__name__) >>> logger.setLevel(logging.DEBUG) >>> >>> >>> @six.add_metaclass(DefaultTraceMeta) >>> class Foo: ... def baz(self, a, b, c=None): ... return True ... def get_blah(self): ... return 5 ... def _hello(self): ... pass ... @disable_trace ... def world(self): ... pass ... def __repr__(self): ... return '<{} object>'.format(self.__class__.__name__,) ... >>> class Bar(Foo): ... def baz(self, a, b, c=None): ... b += 1 ... return super(Bar, self).baz(a, b, c) ... def world(self): ... pass ... @limit_trace_arguments(skip=['password']) ... def secret(self, password, other): ... pass ... @limit_trace_arguments(only=['other']) ... def secret2(self, password, other): ... pass ... >>> a = Foo() >>> a.baz(1, 2, 3) DEBUG:__main__:calling Foo.baz(self=, a=1, b=2, c=3) >>> a.baz(4, b=8) DEBUG:__main__:calling Foo.baz(self=, a=4, b=8) >>> a.get_blah() # nothing happens, since v1.DefaultTraceMeta does not trace "get_.*" >>> a._hello() # nothing happens, since v1.DefaultTraceMeta does not trace "_.*" >>> a.world() # nothing happens, since v1.DefaultTraceMeta does not trace "_.*" >>> b = Bar() >>> b.baz(4, b=8) # tracing is inherited DEBUG:__main__:calling Bar.baz(self=, a=4, b=8) DEBUG:__main__:calling Foo.baz(self=, a=4, b=9, c=None) >>> b.world() # nothing happens, since Foo.world() tracing was disabled and Bar inherited it >>> b.secret('correct horse battery staple', 'Hello world!') DEBUG:__main__:calling Bar.secret(self=, other='Hello world!') (hidden args: password) >>> b.secret2('correct horse battery staple', 'Hello world!') DEBUG:__main__:calling Bar.secret2(other='Hello world!') (hidden args: self, password) ^^^^^^^^^^^^^^^^^^^^ trace_call decorator ^^^^^^^^^^^^^^^^^^^^ .. sourcecode:: pycon >>> import logging >>> from logfury import * >>> logging.basicConfig() >>> logger = logging.getLogger(__name__) >>> >>> @trace_call(logger) ... def foo(a, b, c=None): ... return True ... >>> foo(1, 2, 3) True >>> logger.setLevel(logging.DEBUG) >>> foo(1, 2, 3) DEBUG:__main__:calling foo(a=1, b=2, c=3) True >>> foo(1, b=2) DEBUG:__main__:calling foo(a=1, b=2) True >>> logfury-1.0.1/logfury/000077500000000000000000000000001413474417400147005ustar00rootroot00000000000000logfury-1.0.1/logfury/__init__.py000066400000000000000000000000001413474417400167770ustar00rootroot00000000000000logfury-1.0.1/logfury/_logfury/000077500000000000000000000000001413474417400165265ustar00rootroot00000000000000logfury-1.0.1/logfury/_logfury/__init__.py000066400000000000000000000000001413474417400206250ustar00rootroot00000000000000logfury-1.0.1/logfury/_logfury/meta.py000066400000000000000000000117211413474417400200300ustar00rootroot00000000000000from abc import ABCMeta import logging from .trace_call import trace_call class AbstractTraceMeta(type): """ An abstract metaclass for tracing classes """ @classmethod def _filter_attribute(mcs, attribute_name, attribute_value): """ decides whether the given attribute should be excluded from tracing or not """ if attribute_name == '__module__': return True elif hasattr(attribute_value, '_trace_disable'): return True return False def __new__(mcs, name, bases, attrs, **kwargs): # *magic*: an educated guess is made on how the module that the # processed class is created in would get its logger. # It is assumed that the popular convention recommended by the # developers of standard library (`logger = logging.getLogger(__name__)`) # is used. target_logger = logging.getLogger(attrs['__module__']) for attribute_name in attrs: attribute_value = attrs[attribute_name] if mcs._filter_attribute(attribute_name, attribute_value): continue # attrs['__module__'] + '.' + attribute_name is worth logging # collect the `only` and `skip` sets from mro only = getattr(attribute_value, '_trace_only', None) skip = getattr(attribute_value, '_trace_skip', None) disable = False for base in bases: base_attribute_value = getattr(base, attribute_name, None) if base_attribute_value is None: continue # the base class did not define this if hasattr(base_attribute_value, '_trace_disable'): # that's probably done by @disable_trace # ex. inheriting from Abstract class, where getters are marked disable = True break only_candidates = getattr(base_attribute_value, '_trace_only', None) if only_candidates is not None: if only is not None: only.update(only_candidates) else: only = set(only_candidates) skip_candidates = getattr(base_attribute_value, '_trace_skip', None) # is this 5 LOC clone worth refactoring? if skip_candidates is not None: if skip is not None: skip.update(skip_candidates) else: skip = set(skip_candidates) if disable: continue # the base class does not wish to trace it at all # create a wrapper (decorator object) wrapper = trace_call( target_logger, only=only, skip=skip, ) original_wrapper = None # Special case for staticmethod/classmethod if isinstance(attribute_value, staticmethod): attribute_value = attribute_value.__func__ original_wrapper = staticmethod elif isinstance(attribute_value, classmethod): attribute_value = attribute_value.__func__ original_wrapper = classmethod # wrap the callable in it wrapped_value = wrapper(attribute_value) # apply the original wrapper if provided if original_wrapper is not None: wrapped_value = original_wrapper(wrapped_value) # substitute the trace-wrapped method for the original attrs[attribute_name] = wrapped_value return super(AbstractTraceMeta, mcs).__new__(mcs, name, bases, attrs) class TraceAllPublicCallsMeta(AbstractTraceMeta): """ traces all public method calls """ @classmethod def _filter_attribute(mcs, attribute_name, attribute_value): if super(TraceAllPublicCallsMeta, mcs)._filter_attribute(attribute_name, attribute_value): return True elif not callable(attribute_value): # Special case for staticmethod/classmethod as prior to Python 3.10 # staticmethod/classmethod are not callable if not isinstance(attribute_value, (classmethod, staticmethod)): return True # it is a field elif attribute_name.startswith('_'): return True # it is a _protected or a __private method (or __magic__) return False class AbstractTracePublicCallsMeta(ABCMeta, TraceAllPublicCallsMeta): pass class DefaultTraceMeta(TraceAllPublicCallsMeta): """ traces all public method calls, except for ones with names that begin with 'get_' """ @classmethod def _filter_attribute(mcs, attribute_name, attribute_value): if super(DefaultTraceMeta, mcs)._filter_attribute(attribute_name, attribute_value): return True elif attribute_name.startswith('get_'): return True return False class DefaultTraceAbstractMeta(ABCMeta, DefaultTraceMeta): pass logfury-1.0.1/logfury/_logfury/trace_call.py000066400000000000000000000066351413474417400212030ustar00rootroot00000000000000from collections import OrderedDict from functools import wraps import logging from inspect import isclass, signature class trace_call: """ A decorator which causes the function execution to be logged using a passed logger """ LEVEL = logging.DEBUG def __init__(self, logger, only=None, skip=None): """ only - if not None, contains a whitelist (tuple of names) of arguments that are safe to be logged. All others can not be logged. skip - if not None, contains a whitelist (tuple of names) of arguments that are not safe to be logged. """ self.logger = logger self.only = only self.skip = skip def __call__(self, callable_obj): is_class = isclass(callable_obj) if is_class: function = callable_obj.__init__ else: function = callable_obj @wraps(function) def wrapper(*wrapee_args, **wrapee_kwargs): if self.logger.isEnabledFor(self.LEVEL): args_dict = OrderedDict() sig = signature(function) bound = sig.bind(*wrapee_args, **wrapee_kwargs) for param in sig.parameters.values(): if param.name not in bound.arguments: args_dict[param.name] = param.default else: args_dict[param.name] = bound.arguments[param.name] if is_class: args_dict.popitem(last=False) # remove "self" # filter arguments output_arg_names = [] skipped_arg_names = [] if self.skip is not None and self.only is not None: for arg in args_dict.keys(): if arg in self.only and arg not in self.skip: output_arg_names.append(arg) else: skipped_arg_names.append(arg) elif self.only is not None: for arg in args_dict.keys(): if arg in self.only: output_arg_names.append(arg) else: skipped_arg_names.append(arg) elif self.skip is not None: for arg in args_dict.keys(): if arg in self.skip: skipped_arg_names.append(arg) else: output_arg_names.append(arg) else: output_arg_names = args_dict # format output suffix = '' if skipped_arg_names: suffix = ' (hidden args: {})'.format(', '.join(skipped_arg_names)) arguments = ', '.join('{}={}'.format(k, repr(args_dict[k])) for k in output_arg_names) function_name = getattr(function, '__qualname__', function.__name__) if is_class: function_name, *_ = function_name.rpartition('.') # remove "__init__" # actually log the call self.logger.log(self.LEVEL, 'calling %s(%s)%s', function_name, arguments, suffix) return function(*wrapee_args, **wrapee_kwargs) if is_class: callable_obj.__init__ = wrapper return callable_obj else: return wrapper logfury-1.0.1/logfury/_logfury/tuning.py000066400000000000000000000015171413474417400204100ustar00rootroot00000000000000class limit_trace_arguments: """ A decorator which causes the function execution logging to omit some fields """ def __init__(self, only=None, skip=None): """ only - if not None, contains a whitelist (tuple of names) of arguments that are safe to be logged. All others can not be logged. skip - if not None, contains a whitelist (tuple of names) of arguments that are not safe to be logged. """ self.only = only self.skip = skip def __call__(self, function): function._trace_only = self.only function._trace_skip = self.skip return function def disable_trace(function): """ A decorator which suppresses the function execution logging """ function._trace_disable = True return function logfury-1.0.1/logfury/v0_1/000077500000000000000000000000001413474417400154455ustar00rootroot00000000000000logfury-1.0.1/logfury/v0_1/__init__.py000066400000000000000000000000231413474417400175510ustar00rootroot00000000000000from ..v1 import * logfury-1.0.1/logfury/v1/000077500000000000000000000000001413474417400152265ustar00rootroot00000000000000logfury-1.0.1/logfury/v1/__init__.py000066400000000000000000000005341413474417400173410ustar00rootroot00000000000000from .._logfury.meta import AbstractTracePublicCallsMeta from .._logfury.meta import DefaultTraceAbstractMeta from .._logfury.meta import DefaultTraceMeta from .._logfury.meta import TraceAllPublicCallsMeta from .._logfury.trace_call import trace_call from .._logfury.tuning import disable_trace from .._logfury.tuning import limit_trace_arguments logfury-1.0.1/noxfile.py000066400000000000000000000055731413474417400152410ustar00rootroot00000000000000import os from glob import glob import nox CI = os.environ.get('CI') is not None NOX_PYTHONS = os.environ.get('NOX_PYTHONS') PYTHON_VERSIONS = [ '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', ] if NOX_PYTHONS is None else NOX_PYTHONS.split(',') PYTHON_DEFAULT_VERSION = PYTHON_VERSIONS[-1] PY_PATHS = ['logfury', 'test', 'noxfile.py', 'setup.py'] REQUIREMENTS_FORMAT = ['yapf==0.27'] REQUIREMENTS_LINT = [*REQUIREMENTS_FORMAT, 'flake8==4.0.1', 'pytest==6.2.5'] REQUIREMENTS_TEST = [ "pytest==6.2.5;python_version>'3.5'", "pytest==6.1.1;python_version=='3.5'", "pytest-cov==3.0.0;python_version>'3.5'", "pytest-cov==2.10.1;python_version=='3.5'", "testfixtures==6.18.3", ] REQUIREMENTS_COVER = ['cover'] REQUIREMENTS_BUILD = ['setuptools>=20.2'] nox.options.reuse_existing_virtualenvs = True nox.options.sessions = [ 'lint', 'test', ] # In CI, use Python interpreter provided by GitHub Actions if CI: nox.options.force_venv_backend = 'none' def install_myself(session, extras=None): """Install from the source.""" arg = '.' if extras: arg += '[{}]'.format(','.join(extras)) session.install('-e', arg) @nox.session(name='format', python=PYTHON_DEFAULT_VERSION) def format_(session): """Format the code.""" session.install(*REQUIREMENTS_FORMAT) session.run('yapf', '--in-place', '--parallel', '--recursive', *PY_PATHS) @nox.session(python=PYTHON_DEFAULT_VERSION) def lint(session): """Run linters.""" install_myself(session) session.install(*REQUIREMENTS_LINT) session.run('yapf', '--diff', '--parallel', '--recursive', *PY_PATHS) session.run('flake8', *PY_PATHS) @nox.session(python=PYTHON_VERSIONS) def test(session): """Run unit tests.""" install_myself(session) session.install(*REQUIREMENTS_TEST) args = ['--cov=logfury', '--cov-branch', '--cov-report=xml', '--doctest-modules'] session.run('pytest', *args, *session.posargs, 'test') if not session.posargs: session.notify('cover') @nox.session def cover(session): """Perform coverage analysis.""" session.install(*REQUIREMENTS_COVER) session.run('coverage', 'report', '--fail-under=75', '--show-missing', '--skip-covered') session.run('coverage', 'erase') @nox.session(python=PYTHON_DEFAULT_VERSION) def build(session): """Build the distribution.""" session.install(*REQUIREMENTS_BUILD) session.run('python', 'setup.py', 'check', '--metadata', '--strict') session.run('rm', '-rf', 'build', 'dist', 'logfury.egg-info', external=True) session.run('python', 'setup.py', 'bdist_wheel', *session.posargs) # Set outputs for GitHub Actions if CI: asset_path = glob('dist/*')[0] print('::set-output name=asset_path::', asset_path, sep='') version = os.environ['GITHUB_REF'].replace('refs/tags/v', '') print('::set-output name=version::', version, sep='') logfury-1.0.1/setup.cfg000066400000000000000000000005171413474417400150350ustar00rootroot00000000000000[yapf] based_on_style=facebook COLUMN_LIMIT=140 SPACE_BETWEEN_ENDING_COMMA_AND_CLOSING_BRACKET=False SPLIT_PENALTY_AFTER_OPENING_BRACKET=0 [flake8] ignore=W503,D100,D105,D202 per-file-ignores= __init__.py:F401,F403 setup.py:E221,E251,E266 test/*:E741 max-line-length=140 max-complexity=20 doctests=1 [coverage:run] branch=true logfury-1.0.1/setup.py000066400000000000000000000043731413474417400147320ustar00rootroot00000000000000# noqa import os.path from codecs import open from setuptools import find_packages, setup ################################################################### yapf: disable NAME = 'logfury' AUTHOR = 'Pawel Polewicz' AUTHOR_EMAIL = 'p.polewicz@gmail.com' DESCRIPTION = 'Toolkit for responsible, low-boilerplate logging of library method calls', LICENSE = 'BSD' KEYWORDS = ['logging', 'tracing'] URL = 'https://github.com/reef-technologies/logfury' DOWNLOAD_URL = URL + '/releases' CLASSIFIERS = [ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Logging', 'Natural Language :: English', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ] ################################################################### yapf: enable here = os.path.abspath(os.path.dirname(__file__)) def read_file_contents(filename): with open(os.path.join(here, filename), 'rb', encoding='utf-8') as f: return f.read() setup( name = NAME, url = URL, download_url = DOWNLOAD_URL, author = AUTHOR, author_email = AUTHOR_EMAIL, maintainer = AUTHOR, maintainer_email = AUTHOR_EMAIL, packages = find_packages(exclude=['test*']), license = LICENSE, description = DESCRIPTION, long_description = read_file_contents('README.rst'), keywords = KEYWORDS, classifiers = CLASSIFIERS, package_data = {NAME: ['requirements.txt', 'LICENSE']}, setup_requires = ['setuptools_scm<6.0'], # setuptools_scm>=6.0 doesn't support Python 3.5 use_scm_version = True, ) # yapf: disable logfury-1.0.1/test/000077500000000000000000000000001413474417400141705ustar00rootroot00000000000000logfury-1.0.1/test/__init__.py000066400000000000000000000000001413474417400162670ustar00rootroot00000000000000logfury-1.0.1/test/conftest.py000066400000000000000000000000001413474417400163550ustar00rootroot00000000000000logfury-1.0.1/test/v1/000077500000000000000000000000001413474417400145165ustar00rootroot00000000000000logfury-1.0.1/test/v1/__init__.py000066400000000000000000000000001413474417400166150ustar00rootroot00000000000000logfury-1.0.1/test/v1/test_abstract_meta.py000066400000000000000000000044211413474417400207410ustar00rootroot00000000000000from abc import abstractmethod import pytest from testfixtures import LogCapture from logfury.v1 import AbstractTracePublicCallsMeta, DefaultTraceAbstractMeta class TestTraceAllPublicCallsMeta: def test_subclass(self): class Supp(metaclass=AbstractTracePublicCallsMeta): @abstractmethod def a(self): pass def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Ala(Supp): def a(self): pass def bar(self, a, b, c=None): return True a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=3)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=None)'.format(a.bar.__qualname__)), ) class Bela(Supp): # did not define a() def bar(self, a, b, c=None): return True with pytest.raises(TypeError): Bela() class TestDefaultTraceAbstractMeta(TestTraceAllPublicCallsMeta): def test_subclass(self): class Supp(metaclass=DefaultTraceAbstractMeta): @abstractmethod def a(self): pass def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Ala(Supp): def a(self): pass def bar(self, a, b, c=None): return True a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=3)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=None)'.format(a.bar.__qualname__)), ) class Bela(Supp): # did not define a() def bar(self, a, b, c=None): return True with pytest.raises(TypeError): Bela() logfury-1.0.1/test/v1/test_meta.py000066400000000000000000000206331413474417400170610ustar00rootroot00000000000000from testfixtures import LogCapture from logfury.v1 import TraceAllPublicCallsMeta, limit_trace_arguments, disable_trace class TestTraceAllPublicCallsMeta: def test_subclass(self): class Ala(metaclass=TraceAllPublicCallsMeta): def bar(self, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(Ala): def bar(self, a, b, c=None): return False with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=3)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=None)'.format(a.bar.__qualname__)), ) with LogCapture() as l: b = Bela() b.bar(1, 2, 3) b.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=3)'.format(b.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, a=1, b=2, c=None)'.format(b.bar.__qualname__)), ) def test_disable_trace(self): class Ala(metaclass=TraceAllPublicCallsMeta): @disable_trace def bar(self, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(Ala): def bar(self, a, b, c=None): return False with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) b = Bela() b.bar(1, 2, 3) b.bar(1, b=2) l.check() def test_empty_only(self): class Ala(metaclass=TraceAllPublicCallsMeta): @limit_trace_arguments(only=tuple()) def bar(self, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(Ala): def bar(self, a, b, c=None): return False with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) b = Bela() b.bar(1, 2, 3) b.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}() (hidden args: self, a, b, c)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}() (hidden args: self, a, b, c)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}() (hidden args: self, a, b, c)'.format(b.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}() (hidden args: self, a, b, c)'.format(b.bar.__qualname__)), ) def test_skip(self): class Ala(metaclass=TraceAllPublicCallsMeta): @limit_trace_arguments(skip=['a']) def bar(self, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(Ala): def bar(self, a, b, c=None): return False with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) b = Bela() b.bar(1, 2, 3) b.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(self=, b=2, c=3) (hidden args: a)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, b=2, c=None) (hidden args: a)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, b=2, c=3) (hidden args: a)'.format(b.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, b=2, c=None) (hidden args: a)'.format(b.bar.__qualname__)), ) def test_only(self): class Ala(metaclass=TraceAllPublicCallsMeta): @limit_trace_arguments(only=['a']) def bar(self, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(Ala): def bar(self, a, b, c=None): return False with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) b = Bela() b.bar(1, 2, 3) b.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(a=1) (hidden args: self, b, c)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(a=1) (hidden args: self, b, c)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(a=1) (hidden args: self, b, c)'.format(b.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(a=1) (hidden args: self, b, c)'.format(b.bar.__qualname__)), ) def test_skip_and_only(self): class Ala(metaclass=TraceAllPublicCallsMeta): @limit_trace_arguments(only=['self', 'a', 'b'], skip=['a']) def bar(self, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(Ala): def bar(self, a, b, c=None): return False with LogCapture() as l: a = Ala() a.bar(1, 2, 3) a.bar(1, b=2) b = Bela() b.bar(1, 2, 3) b.bar(1, b=2) l.check( (__name__, 'DEBUG', 'calling {}(self=, b=2) (hidden args: a, c)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, b=2) (hidden args: a, c)'.format(a.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, b=2) (hidden args: a, c)'.format(b.bar.__qualname__)), (__name__, 'DEBUG', 'calling {}(self=, b=2) (hidden args: a, c)'.format(b.bar.__qualname__)), ) def test_class(self): class Ala(metaclass=TraceAllPublicCallsMeta): class Bela: def __init__(self, a, b, c=None): pass def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) with LogCapture() as l: a = Ala() a.Bela(1, 2, 3) Ala.Bela(1, 2, 3) l.check( (__name__, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(a.Bela.__qualname__)), (__name__, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(Ala.Bela.__qualname__)), ) def test_classmethod(self): class MetaAla(TraceAllPublicCallsMeta): def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Ala(metaclass=MetaAla): @classmethod def bar(cls, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) with LogCapture() as l: a = Ala() a.bar(1, 2, 3) l.check((__name__, 'DEBUG', 'calling {}(cls=, a=1, b=2, c=3)'.format(a.bar.__qualname__)),) def test_staticmethod(self): def ala(a, b, c=None): pass class Ala: def __init__(self, a, b, c=None): pass def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Bela(metaclass=TraceAllPublicCallsMeta): foo = staticmethod(ala) Foo = staticmethod(Ala) @staticmethod def bar(a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) with LogCapture() as l: b = Bela() b.foo(1, 2, 3) b.Foo(1, 2, 3) b.bar(1, 2, 3) l.check( (__name__, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(ala.__qualname__)), (__name__, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(b.Foo.__qualname__)), (__name__, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(b.bar.__qualname__)), ) logfury-1.0.1/test/v1/test_trace_call.py000066400000000000000000000077261413474417400202340ustar00rootroot00000000000000import logging from testfixtures import LogCapture from logfury.v1 import trace_call logger_name = "testlogger" logger = logging.getLogger(logger_name) @trace_call(logger) def global_foo(a, b, c=None): return True class TestTraceCall: def test_global_and_local_name(self): @trace_call(logger) def local_foo(a, b, c=None): return True with LogCapture() as l: global_foo(1, 2, 3) local_foo(1, 2, 3) l.check( (logger_name, 'DEBUG', 'calling global_foo(a=1, b=2, c=3)'), (logger_name, 'DEBUG', 'calling TestTraceCall.test_global_and_local_name..local_foo(a=1, b=2, c=3)'), ) def test_all_arguments(self): with LogCapture() as l: @trace_call(logger) def foo(a, b, c=None): return True foo(1, 2, 3) foo(1, b=2) l.check( (logger_name, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(foo.__qualname__)), (logger_name, 'DEBUG', 'calling {}(a=1, b=2, c=None)'.format(foo.__qualname__)), ) def test_complex_signature(self): with LogCapture() as l: @trace_call(logger) def foo(a, b, c, d, e, *varargs, f=None, g='G', h='H', i='ii', j='jj', **varkwargs): pass foo('a', 'b', *['c', 'd'], e='E', f='F', Z='Z', **{'g': 'g', 'h': 'h'}) l.check( ( logger_name, 'DEBUG', "calling {}(a='a', b='b', c='c', d='d', e='E', " "varargs=, f='F', g='g', h='h', " "i='ii', j='jj', varkwargs={{'Z': 'Z'}})".format(foo.__qualname__) ), ) def test_class(self): with LogCapture() as l: @trace_call(logger) class Ala: def __init__(self, a, b, c=None): pass def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) Ala(1, 2, 3) l.check((logger_name, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(Ala.__qualname__)),) def test_class_that_use_self_object_attributes_in_repr(self): with LogCapture() as l: @trace_call(logger) class Ala: def __init__(self, a, b, c=None): self.a = a def __repr__(self): # We use self.a here, so the self argument can not be printed in `trace_call` # wrapper as the object is not yet initialized return '<{} a={}>'.format(self.__class__.__name__, self.a) Ala(1, 2, 3) l.check((logger_name, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(Ala.__qualname__)),) def test_classmethod(self): with LogCapture() as l: class MetaAla(type): def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) class Ala(metaclass=MetaAla): @classmethod @trace_call(logger) def bar(cls, a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) a = Ala() a.bar(1, 2, 3) l.check((logger_name, 'DEBUG', 'calling {}(cls=, a=1, b=2, c=3)'.format(a.bar.__qualname__)),) def test_staticmethod(self): with LogCapture() as l: class Bela: @staticmethod @trace_call(logger) def bar(a, b, c=None): return True def __repr__(self): return '<{} object>'.format(self.__class__.__name__,) with LogCapture() as l: b = Bela() b.bar(1, 2, 3) l.check((logger_name, 'DEBUG', 'calling {}(a=1, b=2, c=3)'.format(b.bar.__qualname__)),)