ftrotta.pycolib package

common_tests

Write less test code; get more tests.

The module provides one main class, CallableTest, and the module function get_test_path().

class ftrotta.pycolib.common_tests.CallableTest

Bases: object

Base class that provides facilities to test a single callable.

This class allows automated input type and value checking for any callable, being it:

  • a module function, or

  • an instance method, or

  • a class method, or

  • a class constructor.

Warning

A new test case is needed for every function/method. A single case can test only one function/method, the so-called “callable under test” (CUT).

The CUT is automatically tested for:

A fixture callable_test_config method that returns a CallableTestConfig must be implemented in the derived test case, as shown in the following examples.

Examples

>>> from apackage import amodule as mut
>>> from ftrotta.pycolib.common_tests import (
>>>     CallableTest,
>>>     MultipleTypeArg, OptionalArg, OptionalMultipleTypeArg
>>> )
>>> from ftrotta.pycolib.log import get_configured_root_logger
>>>
>>> _logger = get_configured_root_logger()
>>>
>>> class TestModuleFunction(CallableTest):
>>>
>>>     @classmethod
>>>     @pytest.fixture(scope="class")
>>>     def callable_test_config(cls):
>>>         config = CallableTestConfig(
>>>             callable_under_test=(mut.afunction,), # MIND THE COMMA!
>>>             default_arg_values_for_tests={
>>>                 'str_or_int': 5,
>>>                 'f': 1.0,
>>>                 's': 'ciao',
>>>                 'd': {},
>>>                 'int_or_dict': MultipleTypeArg(23,  {'a': 23}),
>>>                 'optional': OptionalArg(1),
>>>                 'float_or_int_optional':
>>>                     OptionalMultipleTypeArg(0.1, 12),
>>>             },
>>>             wrong_value_lists = {
>>>                 'str_or_int': [-1],
>>>                 'f': [-1.0],
>>>                 's': [],  # any string is valid
>>>                 'd': [],  # any dictionary is valid
>>>                 'int_or_dict': [-1],
>>>                 'optional': [],  # any int is valid,
>>>                 'float_or_int_optional': [],
>>>             },
>>>         )
>>>         return config
>>>
>>>     # test_input_types is automatically provided by
>>>     # CallableTest
>>>
>>>     # test_input_values is automatically provided by
>>>     # CallableTest
>>>
>>>     # test_basic_behaviour is automatically provided by
>>>     # CallableTest
>>>  class TestModuleFunctionWithOutput(CallableTest):
>>>
>>>     @classmethod
>>>     @pytest.fixture(scope="class")
>>>     def callable_test_config(cls):
>>>         config = CallableTestConfig(
>>>              callable_under_test=(am.double_float,),
>>>              default_arg_values_for_tests={
>>>                  "value": 2.
>>>              },
>>>              wrong_value_lists = {
>>>                  "value": []
>>>              },
>>>              # By setting expected_output, the corresponding
>>>              # verification is triggered. This is applicable
>>>              # only in case of no multiple/optional arguments are
>>>              # are present.
>>>              expected_output=4.0,
>>>         )
>>>         return config
>>> class TestInstanceMethod(CallableTest):
>>>
>>>     @classmethod
>>>     @pytest.fixture(scope="class")
>>>     def callable_test_config(cls):
>>>         # The same as config above, but for the scope.
>>>         # Remark: function scope is used (rather than
>>>         #  class) in order to have a fresh object for each
>>>         #  test.
>>>         an_instance = mut.AClass()
>>>         config = CallableTestConfig(
>>>              callable_under_test=\
>>>                  (an_instance.a_method,),  # MIND THE COMMA!
>>>              ...
>>>          )
>>>          return config
>>>
>>> class TestConstructor(CallableTest):
>>>
>>>     @classmethod
>>>     @pytest.fixture(scope="class")
>>>     def callable_test_config(cls):
>>>         # The same as config above, but for the scope.
>>>         # Remark: function scope is used (rather than
>>>         #  class) in order to have a fresh object for each
>>>         #  test.
>>>         config = CallableTestConfig(
>>>              callable_under_test=\
>>>                  (mut.AClass,),  # MIND THE COMMA!
>>>              ...
>>>          )
>>>          return config
>>>
>>> class TestClassMethod(CallableTest):
>>>
>>>     @classmethod
>>>     @pytest.fixture(scope="class")
>>>     def callable_test_config(cls):
>>>         # The same as config above, but for the scope.
>>>         # Remark: function scope is used (rather than
>>>         #  class) in order to have a fresh object for each
>>>         #  test.
>>>         config = CallableTestConfig(
>>>              callable_under_test=\
>>>                  (mut.AClass.a_class_method,),  # MIND THE COMMA!
>>>              ...
>>>          )
>>>          return config
classmethod get_some_types() List
Returns

List of items with different types for test_input_types().

It can be overridden by the child class, to provide different types to check against.

Examples

>>>  @classmethod
>>>  def get_some_types(self):
>>>     l = CallableTest.get_some_types()
>>>     l.append(np.zeros([2, 2]))
>>>     return l
test_basic_behaviour(callable_test_config: ftrotta.pycolib.common_tests.CallableTestConfig)

Check that no exception is raised when the callable is invoked with inputs defined in default_arg_values_for_tests.

Warning

In case OptionalArg or MultipleTypeArg or OptionalMultipleTypeArg are used, this leads to multiple effective inputs to be checked. In doing so, no copies of the inputs are created. This means that any side effect of the callable on any of its inputs could impair the test.

If your callable under test has side effects that could influence the output, please: (1) skip this test by setting skip_test_basic_behaviour to True; and (2) perform custom behavioural testing, by implementing your test functions.

If CallableTestConfig.expected_output is not None, output checking is triggered.

test_input_types(callable_test_config: ftrotta.pycolib.common_tests.CallableTestConfig)

Check that the callable raises a TypeError when an input with type different from that in default_arg_values_for_tests is given.

The values that are used as wrong type values can be managed in get_some_types().

test_input_values(callable_test_config: ftrotta.pycolib.common_tests.CallableTestConfig)

Check that the callable raises a ValueError when an input with value defined in wrong_value_lists is given.

class ftrotta.pycolib.common_tests.CallableTestConfig(*, callable_under_test: Tuple[Callable], default_arg_values_for_tests: Dict[str, Any], wrong_value_lists: Dict[str, List], expected_output: Optional[Any] = None, skip_test_basic_behaviour: bool = False)

Bases: object

Configuration for test callable classes.

Please refer to CallableTest.

Parameters
  • callable_under_test – Mind the syntax of the tuple: (callable,).

  • default_arg_values_for_tests

  • wrong_value_lists

  • expected_output – defaults to None.

  • skip_test_basic_behaviour – defaults to False.

callable_under_test: Tuple[Callable]

The callable under test.

Mind the syntax of the tuple: (callable,).

See also the examples of CallableTest.

default_arg_values_for_tests: Dict[str, Any]

Every argument of the callable needs to be defined.

OptionalArg, MultipleTypeArg and OptionalMultipleTypeArg can be used.

See also the examples of CallableTest.

expected_output: Optional[Any]

See also CallableTest.test_basic_behaviour().

skip_test_basic_behaviour: bool

See also CallableTest.test_basic_behaviour().

wrong_value_lists: Dict[str, List]

See also the examples of CallableTest.

exception ftrotta.pycolib.common_tests.CallableUnderTestNotTupleError

Bases: ftrotta.pycolib.common_tests.CommonTestsException

exception ftrotta.pycolib.common_tests.CommonTestsException

Bases: Exception

Base class for exceptions.

exception ftrotta.pycolib.common_tests.MissingArgumentError

Bases: ftrotta.pycolib.common_tests.CommonTestsException

exception ftrotta.pycolib.common_tests.MissingConfigurationError

Bases: ftrotta.pycolib.common_tests.CommonTestsException

class ftrotta.pycolib.common_tests.MultipleTypeArg(*args)

Bases: object

exception ftrotta.pycolib.common_tests.NotCallableError

Bases: ftrotta.pycolib.common_tests.CommonTestsException

class ftrotta.pycolib.common_tests.OptionalArg(arg=None)

Bases: object

class ftrotta.pycolib.common_tests.OptionalMultipleTypeArg(*args)

Bases: object

ftrotta.pycolib.common_tests.get_test_path(module_name: str, relative_path: Union[str, pathlib.Path]) Union[str, pathlib.Path]

Computes paths for testing.

When running tests from the IDE, the working directory changes. This makes it difficult to define paths to files/directories, without knowing the project path, as it is the case when running in different environments.

This function allows to define a path that will work for unittest and pytest regardless of the current working directory.

Parameters
  • module_name – As obtained by __name__.

  • relative_path – The path to file/dir relative to the test definition file.

Returns

The absolute path to file/dir. It has the same type of relative_path.

Examples

>>> import os
>>> from typing import Union
>>> from pathlib import Path
>>> from ftrotta.pycolib.common_tests import get_test_path
>>>
>>>
>>> def gtp(relative_path: Union[str, Path]) -> Union[str, Path]:
>>>     return get_test_path(__name__, relative_path)
>>>
>>>
>>> def test(self):
>>>     fname = gtp(Path('files/')) / 'foo.png'
>>>     fname = gtp('files/foo.png')
>>>

input_checks

A module with facilities to check the type and range of input parameters.

ftrotta.pycolib.input_checks.check_dict_has_key(par: Any, par_name: str, key: Any) None

Check par to be a dictionary that includes a given key.

Parameters
  • par

  • par_name

  • key

Raises
  • TypeError – if par type is not dict.

  • ValueError – if par has not the key.

ftrotta.pycolib.input_checks.check_dict_has_key_with_type(par: Any, par_name: str, key: Any, key_type: Type) None

Check par to be a dictionary that includes a given key of a given type.

Parameters
  • par

  • par_name

  • key

  • key_type

Raises
  • TypeError – if par type is not dict.

  • ValueError – if par has not the key or the key type is not key_type.

ftrotta.pycolib.input_checks.check_homogeneous_list(par: Any, par_name: str, expected_type: Type) None

Check that all elements have same type.

Parameters
  • par

  • par_name

  • expected_type

Raises
  • TypeError – if par is not a list.

  • ValueError – if at least one element has not type expected_type.

ftrotta.pycolib.input_checks.check_in_range(par: Any, par_name: str, min_value: Any, max_value: Any, include_min: bool, include_max: bool) None

Check par to be within (min_value, max_value).

Parameters
  • par – Same type as value_min.

  • par_name

  • min_value – Same type as value_max, less than it.

  • max_value – Same type as value_min, grater than it.

  • include_min

  • include_max

Raises
  • AssertionError – If input pars different from par do not have the right type/value.

  • TypeError – If not isinstance(par, type(min_value)).

  • ValueError – If par is not within the range.

ftrotta.pycolib.input_checks.check_isdir(par: Any, par_name: str) None

Checks par to be a pathlib.Path and an existing directory.

Parameters
  • par (any) –

  • par_name (str) –

Raises
  • TypeError – if par type is not pathlib.Path.

  • ValueError – if par value is not an existing directory.

ftrotta.pycolib.input_checks.check_isfile(par: Any, par_name: str) None

Checks par to be a pathlib.Path and an existing file.

Parameters
  • par

  • par_name

Raises
  • TypeError – if par type is not :class:`pathlib.Path.

  • ValueError – if par value is not an existing file.

ftrotta.pycolib.input_checks.check_multiple_type(par: Any, par_name: str, expected_types: Tuple[Type, ...]) None

Check the parameter against a list of possible types.

Examples

>>> from ftrotta.pycolib import input_checks as ics
>>>
>>> def f(a):
>>>     ics.check_multiple_type(a, 'a', (int, double))
>>>     pass
Parameters
  • par

  • par_name

  • expected_types

Raises

TypeError

ftrotta.pycolib.input_checks.check_ndarray_with_type(par: Any, par_name: str, np_type: numpy.dtype) None

Check that par is a ndarray whose type is np_type.

Parameters
  • par

  • par_name

  • np_type

Raises
  • TypeError – if par type is not ndarray.

  • ValueError – if the type of the elements of par is not np_type.

ftrotta.pycolib.input_checks.check_noneable_multiple_type(par: Any, par_name: str, expected_type: Tuple[Type, ...]) None

Check the parameter against a tuple of possible types, if not None.

Examples

>>> from ftrotta.pycolib import input_checks as ics
>>>
>>> def f(a):
>>>     ics.check_multiple_type(a, 'a', (int, double))
>>>     pass
Parameters
  • par

  • par_name

  • expected_type

Raises

TypeError

ftrotta.pycolib.input_checks.check_noneable_type(par: Any, par_name: str, expected_type: Type) None

Check the type of par, if it is not None.

Parameters
  • par

  • par_name

  • expected_type

Raises

TypeError

ftrotta.pycolib.input_checks.check_nonempty_str(par: Any, par_name: str) None
Parameters
  • par (str) –

  • par_name (str) –

Raises
  • TypeError – if par is not a string.

  • ValueError – if par is an empty string.

ftrotta.pycolib.input_checks.check_nonnegativefloat(par: Any, par_name: str) None

Check par to be float and non-negative.

Parameters
  • par

  • par_name

Raises
  • TypeError – if par type is not float.

  • ValueError – if par value is not non-negative.

ftrotta.pycolib.input_checks.check_nonnegativeint(par: Any, par_name: str) None

Check par to be int and non-negative.

Parameters
  • par

  • par_name

Raises
  • TypeError – if par type is not int.

  • ValueError – if par value is not non-negative.

ftrotta.pycolib.input_checks.check_positivefloat(par: Any, par_name: str) None

Check par to be float and positive.

Parameters
  • par

  • par_name

Raises
  • TypeError – if par type if not float.

  • ValueError – if par value is not positive.

ftrotta.pycolib.input_checks.check_positiveint(par: Any, par_name: str) None

Checks par to be int and positive.

Parameters
  • par (any) –

  • par_name (str) –

Raises
  • TypeError – if par type if not int.

  • ValueError – if par value is not positive.

ftrotta.pycolib.input_checks.check_tuple_with_len(par: Any, par_name: str, length: int) None

Check par to be a tuple with given length.

Parameters
  • par

  • par_name

  • length – positive.

Raises
  • AssertionError – if length is not positive int.

  • TypeError – if par is not a tuple.

  • ValueError – if len(par) != length.

ftrotta.pycolib.input_checks.check_type(par: Any, par_name: str, expected_type: Type) None

Check the parameters against a single type.

Examples

>>> from ftrotta.pycolib import input_checks as ics
>>>
>>> def f(a):
>>>     ics.check_type(a, 'a', int)
>>>     pass
Parameters
  • par

  • par_name

  • expected_type

Raises

TypeError

log

Logging facilities.

The name prevents conflicts with default logging package.

ftrotta.pycolib.log.get_configured_root_logger(level: int = 10) logging.Logger

Get the root logger with configured handler and formatter.

It can be used in any top level module, to handle logging on stderr. The stream handler on stderr is added only in case the root logger has no other handlers. This prevents logging message repetitions when used in test modules.

Example

>>> from ftrotta.pycolib.log import get_configured_root_logger
>>>
>>> _logger = get_configured_root_logger()
>>>
>>> _logger.debug('Debug message')
Parameters

level – The logging level (set to the handler). Defaults to logging.DEBUG.

Returns

The root logger, with configured handler on stderr and formatter.

setup

Module with facilities to simplify the managments of setup.py.

The idea is to write as few code as possible, extensively relaying on automation, to allow for easy refactoring of your projects.

PEP 518 compliancy is not relevant at the moment.

Examples

>>> # setup.py
>>> from setuptools import setup
>>> from ftrotta.pycolib.setup import infer_package_info
>>>
>>> SRC_PATH = 'src/'
>>>
>>> name, project_urls, packages = infer_package_info(
>>>     where=SRC_PATH, group='sbrin', rtfd=True)
>>>
>>> with open('README.md', 'r', encoding='utf-8') as fh:
>>>     long_description = fh.read()
>>>
>>> install_requires = get_install_requires('requirements.txt', 'gitlab')
>>>
>>> setup(
>>>     name=name,
>>>     use_scm_version=True,
>>>     project_urls=project_urls,
>>>     author='Sergey Brin',
>>>     description='A new Internet serach engine.',
>>>     long_description=long_description,
>>>     long_description_content_type='text/markdown',
>>>     package_dir={'': SRC_PATH},
>>>     packages=packages,
>>>     setup_requires=[
>>>         'setuptools_scm',
>>>     ],
>>>     install_requires=install_requires,
>>>     classifiers=[
>>>         'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
>>>         'Operating System :: OS Independent',
>>>         'Programming Langusphinx-build -W . _build/html/age :: Python :: 3',
>>>     ],
>>>     python_requires='>=3.6',
>>> )
ftrotta.pycolib.setup.get_install_requires(fname: str, schema: str) List[str]

Get the install_requires list from requirements.txt file.

Warning

This function is deprecated since v1.2.2 as there is no motivation for it. The syntax package@git+https://gitlab.com/group/project.git is, indeed, accepted by pip as well.

The main goal is to manage the git+ requirements, such as git+https://gitlab.com/foo/bar.git. Such a requirement is accepted in requirements.txt, but not in `install_requires of setup.py. For this to happen it must be converted to <package>@git+<url>. Please refer to https://stackoverflow.com/questions/55385900/pip3-setup-py-install-requires-pep-508-git-url-for-private-repo.

The function also parses files included via the ‘-r’ syntax. It ignores the comment and empty lines.

Parameters
  • fname

  • schema – only ‘gitlab’ is accepted. It converts git+https://gitlab.com/foo/bar.git to foo.bar@git+https://gitlab.com/foo/bar.git.

Returns

The list of requirements to be used as install_requires in setup.py.

ftrotta.pycolib.setup.infer_package_info(where: str, group: str, rtfd: bool) Tuple[str, Dict[str, str], List[str]]

Infer package information from the filesystem.

It assumes that there is only one main package, that is the object of the development of the repository.

Such project package is assumed to be second-level subpackage. In particular a structure like <group>.<project_package> is assumed. This complies with PEP423 Use single name.

The first level package, namely <group> represents the author or the organization. It can a namespace package, in the form of either a native namespace package, that has no __init__.py file in it, or a pkgutil-style namespace package. Please recall that the strategy cannot be changed and all projects need to share the same method for the namespace package.

Parameters
  • where – Path where to look for packages, the source directory.

  • group – The name of the author or organization.

  • rtfd – Whether the documentation is hosted in ReadTheDocs. Alternatively, it is considered to be hosted in the Gitlab Pages of the project.

Returns

(project_package, project_urls, pkg_list)

  • project_package: The name of the main package. It can be used for the name parameter of setup.py.

  • project_urls: The Source Code and Documentation URLs. A project hosted in Gitlab.com is assumed.

  • pkg_list: The list of packages to be installed.