diff --git a/.codeclimate.yml b/.codeclimate.yml index c579fd98302bbc230dea126b5ed41dbecb5f532a..1cb06657e4f8399818264cf16f07648e1fa632b7 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -8,6 +8,19 @@ plugins: enabled: true pylint: enabled: true + checks: + missing-module-docstring: + enabled: false + missing-class-docstring: + enabled: false + missing-function-docstring: + enabled: false + global-statement: + enabled: false + invalid-name: + enabled: false + too-many-arguments: + enabled: false radon: enabled: true exclude_paths: diff --git a/README.md b/README.md index 7e965e8dfc2abd09b8222616a80a86375d780380..43d9aca7cc7769e015bb04f28b084b7748eddadb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ satella ======== -[](https://codeclimate.com/github/piotrmaslanka/satella) +[](https://codeclimate.com/github/piotrmaslanka/satella) [](https://codeclimate.com/github/piotrmaslanka/satella) [](https://pypi.python.org/pypi/satella) [](https://badge.fury.io/py/satella) @@ -9,7 +9,7 @@ satella [](https://github.com/piotrmaslanka/satella) [](https://github.com/pylint-dev/pylint) -Satella is an almost-zero-requirements Python 3.5+ library for writing server applications. It has arisen out of my +Satella is an almost-zero-requirements Python 3.7+ library for writing server applications. It has arisen out of my requirements to have some classes or design patterns handy, and kinda wish-they-were-in-the-stdlib ones. especially those dealing with mundane but useful things. It also runs on PyPy, and most of it runs on Windows (the part not dealing with forking processes, you see). @@ -18,7 +18,9 @@ Satella uses [semantic versioning 2.0](https://semver.org/spec/v2.0.0.html). Satella contains, among other things: -* things to help you manage your [application's configuration](satella/configuration) +* things to help you manage your [application's configuration](satella/configuration) that allows + you to both load a configuration and specify it's schema using only + Python dictionaries * a fully equipped [metrics library](satella/instrumentation/metrics) * alongside a fully metricized [ThreadPoolExecutor](satella/instrumentation/metrics/structures/threadpool.py) * and an exporter to [Prometheus](satella/instrumentation/metrics/exporters/prometheus.py) or really any @@ -29,7 +31,8 @@ Satella contains, among other things: * [FastAPI](https://github.com/Dronehub/fastapi-satella-metrics) * [Django](https://github.com/piotrmaslanka/django-satella-metrics) * [Flask](https://github.com/piotrmaslanka/flask-satella-metrics) -* helpful [exception handlers](satella/exception_handling) +* helpful [exception handlers](satella/exception_handling) as well as capacity to dump all stack frames + along with their local variables for each thread * monitoring [CPU usage](satella/instrumentation/cpu_time/collectors) on the system and by your own process * common programming [idioms and structures](satella/coding) diff --git a/pyproject.toml b/pyproject.toml index ea9de4e3f60ab4ded5042a8ed605491f20d38c44..5f4771da2d7d218e28f11b24263b51df31dcbc64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries" ] -dependencies = ["psutil"] +dependencies = ["psutil>=5.9.8"] [tool.setuptools.dynamic] version = { attr = "satella.__version__" } diff --git a/satella/cassandra/common.py b/satella/cassandra/common.py index 448f63edaf5cd05c0fe0c891fbb398b4511e4593..acb3224d0c8a64a02b262899685601f66984efe1 100644 --- a/satella/cassandra/common.py +++ b/satella/cassandra/common.py @@ -1,5 +1,5 @@ try: from cassandra.cluster import ResponseFuture except ImportError: - class ResponseFuture: + class ResponseFuture: # pylint: disable=too-few-public-methods pass diff --git a/satella/cassandra/future.py b/satella/cassandra/future.py index 8ffa1d1a96515155a48a1f53137be504407e28d2..b0316ce79fdedd83aae6d252e9144d68e526f3c0 100644 --- a/satella/cassandra/future.py +++ b/satella/cassandra/future.py @@ -18,6 +18,6 @@ def wrap_future(future: ResponseFuture) -> Future: fut = Future() fut.set_running_or_notify_cancel() - future.add_callback(lambda result: fut.set_result(result)) - future.add_errback(lambda exception: fut.set_exception(exception)) + future.add_callback(fut.set_result) + future.add_errback(fut.set_exception) return fut diff --git a/satella/cassandra/parallel.py b/satella/cassandra/parallel.py index 05ad59cda32051b685f2b776ca96d5ac5688f425..fc8def3e5938d3368e8e84bb2c6eda8badbe7818 100644 --- a/satella/cassandra/parallel.py +++ b/satella/cassandra/parallel.py @@ -37,7 +37,7 @@ def parallel_for(cursor, query: tp.Union[tp.List[str], str, 'Statement', tp.List """ warnings.warn('This is deprecated and will be removed in Satella 3.0', DeprecationWarning) try: - from cassandra.query import Statement + from cassandra.query import Statement # pylint: disable=import-outside-toplevel query_classes = (str, Statement) except ImportError: query_classes = str @@ -46,11 +46,8 @@ def parallel_for(cursor, query: tp.Union[tp.List[str], str, 'Statement', tp.List query = itertools.repeat(query) futures = [] - for query, args in zip(query, arguments): - if args is None: - future = cursor.execute_async(query) - else: - future = cursor.execute_async(query, args) + for loc_query, args in zip(query, arguments): + future = cursor.execute_async(loc_query) if args is None else cursor.execute_async(loc_query, args) futures.append(future) for future in futures: diff --git a/satella/coding/expect_exception.py b/satella/coding/expect_exception.py index 0caea5e798ca7a73b1bee59aba571b5d1943f293..d54ea94841b6a5e7664a81168d1d4eb9a4892011 100644 --- a/satella/coding/expect_exception.py +++ b/satella/coding/expect_exception.py @@ -34,8 +34,7 @@ class expect_exception: def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: - raise self.else_raise(*self.else_raise_args, - **self.else_raise_kwargs) + raise self.else_raise(*self.else_raise_args, **self.else_raise_kwargs) elif not isinstance(exc_val, self.exc_to_except): return False return True diff --git a/satella/coding/misc.py b/satella/coding/misc.py index 45fc3c11625872e66d6d854ef69f9c709490270e..1152e5e1fc369d2c23277bde603f03ea071f1cc2 100644 --- a/satella/coding/misc.py +++ b/satella/coding/misc.py @@ -62,7 +62,7 @@ def contains(needle, haystack) -> bool: class Closeable: """ - A class that needs to clean up it's own resources. + A class that needs to clean up its own resources. It's destructor calls .close(). Use like this: @@ -168,8 +168,7 @@ def chain_callables(callable1: tp.Callable, callable2: tp.Callable) -> tp.Callab try: res = callable2(res) except TypeError as e: - if 'positional arguments but' in e.args[0] \ - and 'was given' in e.args[0] and 'takes' in e.args[0]: + if 'positional arguments but' in e.args[0] and 'was given' in e.args[0] and 'takes' in e.args[0]: res = callable2() else: raise @@ -195,8 +194,7 @@ def source_to_function(src: tp.Union[tp.Callable, str]) -> tp.Callable[[tp.Any], q = dict(globals()) exec('_precond = lambda x: ' + src, q) return q['_precond'] - else: - return src + return src def update_attr_if_none(obj: object, attr: str, value: tp.Any, @@ -224,14 +222,13 @@ def update_attr_if_none(obj: object, attr: str, value: tp.Any, if val is None: setattr(obj, attr, value) except AttributeError: - if on_attribute_error: - setattr(obj, attr, value) - else: + if not on_attribute_error: raise + setattr(obj, attr, value) return obj -class _BLANK: +class _BLANK: # pylint: disable=too-few-public-methods pass @@ -257,8 +254,7 @@ def update_key_if_true(dictionary: tp.Dict, key: tp.Hashable, value: tp.Any, return dictionary -def get_arguments(function: tp.Callable, *args, **kwargs) -> \ - tp.Dict[str, tp.Any]: +def get_arguments(function: tp.Callable, *args, **kwargs) -> tp.Dict[str, tp.Any]: """ Return local variables that would be defined for given function if called with provided arguments. @@ -276,6 +272,7 @@ def get_arguments(function: tp.Callable, *args, **kwargs) -> \ return _get_arguments(function, False, *args, **kwargs) +# pylint: disable=too-many-locals @rethrow_as(IndexError, TypeError) def _get_arguments(function: tp.Callable, special_behaviour: bool, *args, **kwargs): """ @@ -286,13 +283,11 @@ def _get_arguments(function: tp.Callable, special_behaviour: bool, *args, **kwar local_vars = {} positionals = [param for param in reversed(params) if - param.kind in (Parameter.POSITIONAL_OR_KEYWORD, - Parameter.POSITIONAL_ONLY, - Parameter.VAR_POSITIONAL)] + param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL)] args = list(reversed(args)) arguments_left = set(param.name for param in params) - while len(positionals): + while positionals: arg = positionals.pop() arg_kind = arg.kind arg_name = arg.name @@ -306,8 +301,7 @@ def _get_arguments(function: tp.Callable, special_behaviour: bool, *args, **kwar try: if arg.default != Parameter.empty: raise AttributeError() - else: - break + break except (AttributeError, TypeError): v = arg.default local_vars[arg_name] = v @@ -321,30 +315,28 @@ def _get_arguments(function: tp.Callable, special_behaviour: bool, *args, **kwar keyword_name = keyword.name if keyword.kind == Parameter.VAR_KEYWORD and not special_behaviour: local_vars[keyword_name] = kwargs - else: + continue + try: + v = kwargs.pop(keyword_name) + except KeyError: try: - v = kwargs.pop(keyword_name) - except KeyError: - try: - if Parameter.empty == keyword.default: - if special_behaviour: - v = None - else: - raise TypeError('Not enough keyword arguments') - else: - v = keyword.default - except (AttributeError, TypeError): - continue # comparison was impossible + if Parameter.empty == keyword.default: + if not special_behaviour: + raise TypeError('Not enough keyword arguments') + v = None + else: + v = keyword.default + except (AttributeError, TypeError): + continue # comparison was impossible - local_vars[keyword_name] = v + local_vars[keyword_name] = v for param in params: param_name = param.name if param_name not in local_vars: - if special_behaviour: - local_vars[param_name] = None - else: + if not special_behaviour: raise TypeError('Not enough keyword arguments') + local_vars[param_name] = None return local_vars @@ -373,8 +365,7 @@ def call_with_arguments(function: tp.Callable, arguments: tp.Dict[str, tp.Any]) continue elif param.default == Parameter.empty: raise TypeError('Argument %s not found' % (param_name,)) - else: - continue + continue if param_kind == Parameter.POSITIONAL_ONLY or param_kind == Parameter.POSITIONAL_OR_KEYWORD: args.append(arguments.pop(param_name)) diff --git a/satella/coding/recast_exceptions.py b/satella/coding/recast_exceptions.py index 6c2b34b2f7ef44b4d9ce5663519363abee75ed41..738ced9fe63ea26df01470d9b3c73e9fb082278a 100644 --- a/satella/coding/recast_exceptions.py +++ b/satella/coding/recast_exceptions.py @@ -31,8 +31,7 @@ def silence_excs(*exc_types: ExceptionClassType, returns=None, :raises ValueError: you gave both returns and returns_factory. You can only pass one of them! """ - return rethrow_as(exc_types, None, returns=returns, - returns_factory=returns_factory) + return rethrow_as(exc_types, None, returns=returns, returns_factory=returns_factory) class log_exceptions: @@ -59,8 +58,7 @@ class log_exceptions: log on all exceptions :param swallow_exception: if True, exception will be swallowed """ - __slots__ = ('logger', 'severity', 'format_string', 'locals', 'exc_types', - 'swallow_exception') + __slots__ = 'logger', 'severity', 'format_string', 'locals', 'exc_types', 'swallow_exception' def __init__(self, logger: logging.Logger, severity: int = logging.ERROR, @@ -81,24 +79,20 @@ class log_exceptions: def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: - if not self.analyze_exception(exc_val, (), {}): - return False - else: - return self.swallow_exception + return False if not self.analyze_exception(exc_val, (), {}) else self.swallow_exception return False def analyze_exception(self, e, args, kwargs) -> bool: """Return whether the exception has been logged""" - if isinstance(e, self.exc_types): - format_dict = {'args': args, - 'kwargs': kwargs} - if self.locals is not None: - format_dict.update(self.locals) - format_dict['e'] = e - self.logger.log(self.severity, self.format_string.format(**format_dict), - exc_info=e) - return True - return False + if not isinstance(e, self.exc_types): + return False + format_dict = {'args': args, 'kwargs': kwargs} + if self.locals is not None: + format_dict.update(self.locals) + format_dict['e'] = e + self.logger.log(self.severity, self.format_string.format(**format_dict), + exc_info=e) + return True def __call__(self, fun): if inspect.isgeneratorfunction(fun): @@ -107,10 +101,8 @@ class log_exceptions: # noinspection PyBroadException try: yield from fun(*args, **kwargs) - except Exception as e: - if not self.analyze_exception(e, args, kwargs): - raise - elif not self.swallow_exception: + except Exception as e: # pylint: disable=broad-except + if not self.analyze_exception(e, args, kwargs) or not self.swallow_exception: raise return inner @@ -120,10 +112,8 @@ class log_exceptions: # noinspection PyBroadException try: return fun(*args, **kwargs) - except Exception as e: - if not self.analyze_exception(e, args, kwargs): - raise - elif not self.swallow_exception: + except Exception as e: # pylint: disable=broad-except + if not self.analyze_exception(e, args, kwargs) or not self.swallow_exception: raise return inner @@ -175,12 +165,12 @@ class reraise_as: def inner(*args, **kwargs): try: return fun(*args, **kwargs) - except Exception as e: + except Exception as e: # pylint: disable=broad-except if isinstance(e, self.source): if self.target_exc is not None: raise self.target_exc(*self.args, **self.kwargs) from e - else: - raise + return + raise return inner @@ -234,8 +224,7 @@ class rethrow_as: is used as as decorator :raises ValueError: you specify both returns and returns_factory """ - __slots__ = 'mapping', 'exception_preprocessor', 'returns', '__exception_remapped', \ - 'returns_factory' + __slots__ = 'mapping', 'exception_preprocessor', 'returns', '__exception_remapped', 'returns_factory' def __init__(self, *pairs: ExceptionList, exception_preprocessor: tp.Optional[tp.Callable[[Exception], str]] = repr, @@ -276,8 +265,7 @@ class rethrow_as: elif self.returns_factory is not None: return self.returns_factory() return - else: - return v + return v return inner @@ -292,8 +280,7 @@ class rethrow_as: self.__exception_remapped.was_raised = True if to is None: return True - else: - raise to(self.exception_preprocessor(exc_val)) + raise to(self.exception_preprocessor(exc_val)) def raises_exception(exc_class: tp.Union[ExceptionClassType, tp.Tuple[ExceptionClassType, ...]], @@ -305,8 +292,7 @@ def raises_exception(exc_class: tp.Union[ExceptionClassType, tp.Tuple[ExceptionC clb() except exc_class: return True - else: - return False + return False def catch_exception(exc_class: tp.Union[ExceptionClassType, tp.Tuple[ExceptionClassType, ...]], diff --git a/satella/json.py b/satella/json.py index 9a74d00065d1efc4f5b1a01e58933a421456334c..073412df2b619a4fc6c6fe7f07935d9f438a5009 100644 --- a/satella/json.py +++ b/satella/json.py @@ -1,6 +1,7 @@ import enum import json import typing as tp +import warnings from abc import ABCMeta, abstractmethod from satella.coding.typing import NoneType @@ -42,17 +43,16 @@ class JSONEncoder(json.JSONEncoder): try: v = super().default(o) except TypeError: - dct = {} + v = {} try: - for k, v in o.__dict__.items(): - dct[k] = self.default(v) + for k, val in o.__dict__.items(): + v[k] = self.default(val) except AttributeError: # o has no attribute '__dict__', try with slots try: for slot in o.__slots__: - dct[slot] = self.default(getattr(o, slot)) + v[slot] = self.default(getattr(o, slot)) except AttributeError: # it doesn't have __slots__ either? v = '<an instance of %s>' % (o.__class__.__name__,) - v = dct return v @@ -73,8 +73,10 @@ def write_json_to_file(path: str, value: JSONAble, **kwargs) -> None: :param path: path to the file :param value: JSON-able content - :param kwargs: will be passed to ujson/json's dump + :param kwargs: Legacy argument do not use it, will raise a warning upon non-empty. This never did anything. """ + if kwargs: + warnings.warn('Do not use kwargs, it has no effect', DeprecationWarning) with open(path, 'w') as f_out: f_out.write(JSONEncoder().encode(value))