pytest-xdocker 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Marc Tardif
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytest-xdocker
3
+ Version: 0.0.0
4
+ Summary: Pytest fixture to run docker across test runs.
5
+ Home-page: https://github.com/cr3/pytest-xdocker
6
+ Author: Marc Tardif
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: attrs (>=24.3.0,<25.0.0)
15
+ Requires-Dist: netifaces (>=0.11.0,<0.12.0)
16
+ Requires-Dist: psutil (>=6.1.1,<7.0.0)
17
+ Requires-Dist: pyhamcrest (>=2.1.0,<3.0.0)
18
+ Requires-Dist: pytest (>=7.4.2,<8.0.0)
19
+ Requires-Dist: pytest-cache (>=1.0,<2.0)
20
+ Requires-Dist: pytest-xprocess (>=1.0.2,<2.0.0)
21
+ Project-URL: Repository, https://github.com/cr3/pytest-xdocker
22
+ Description-Content-Type: text/x-rst
23
+
24
+ pytest-xdocker
25
+ ==============
26
+
27
+ `Pytest <http://pytest.org>`_ fixture to run docker across test runs.
28
+
29
+ .. image:: https://img.shields.io/badge/license-MIT-blue.svg
30
+ :target: https://github.com/cr3/pytest-xdocker/blob/master/LICENSE
31
+ :alt: License
32
+ .. image:: https://img.shields.io/pypi/v/pytest-xdocker.svg
33
+ :target: https://pypi.python.org/pypi/pytest-xdocker/
34
+ :alt: PyPI
35
+ .. image:: https://img.shields.io/github/issues-raw/cr3/pytest-xdocker.svg
36
+ :target: https://github.com/cr3/pytest-xdocker/issues
37
+ :alt: Issues
38
+
39
+ Requirements
40
+ ------------
41
+
42
+ You will need the following prerequisites to use pytest-xdocker:
43
+
44
+ - Python 3.9, 3.10, 3.11, 3.12, 3.13
45
+
46
+ Installation
47
+ ------------
48
+
49
+ To install pytest-xdocker:
50
+
51
+ .. code-block:: bash
52
+
53
+ $ pip install pytest-xdocker
54
+
55
+ Usage
56
+ -----
57
+
58
+ TODO
59
+
60
+ Resources
61
+ ---------
62
+
63
+ - `Documentation <https://cr3.github.io/pytest-xdocker/>`_
64
+ - `Release Notes <http://github.com/cr3/pytest-xdocker/blob/master/CHANGES.rst>`_
65
+ - `Issue Tracker <http://github.com/cr3/pytest-xdocker/issues>`_
66
+ - `Source Code <http://github.com/cr3/pytest-xdocker/>`_
67
+ - `PyPi <https://pypi.org/project/pytest-xdocker/>`_
68
+
@@ -0,0 +1,44 @@
1
+ pytest-xdocker
2
+ ==============
3
+
4
+ `Pytest <http://pytest.org>`_ fixture to run docker across test runs.
5
+
6
+ .. image:: https://img.shields.io/badge/license-MIT-blue.svg
7
+ :target: https://github.com/cr3/pytest-xdocker/blob/master/LICENSE
8
+ :alt: License
9
+ .. image:: https://img.shields.io/pypi/v/pytest-xdocker.svg
10
+ :target: https://pypi.python.org/pypi/pytest-xdocker/
11
+ :alt: PyPI
12
+ .. image:: https://img.shields.io/github/issues-raw/cr3/pytest-xdocker.svg
13
+ :target: https://github.com/cr3/pytest-xdocker/issues
14
+ :alt: Issues
15
+
16
+ Requirements
17
+ ------------
18
+
19
+ You will need the following prerequisites to use pytest-xdocker:
20
+
21
+ - Python 3.9, 3.10, 3.11, 3.12, 3.13
22
+
23
+ Installation
24
+ ------------
25
+
26
+ To install pytest-xdocker:
27
+
28
+ .. code-block:: bash
29
+
30
+ $ pip install pytest-xdocker
31
+
32
+ Usage
33
+ -----
34
+
35
+ TODO
36
+
37
+ Resources
38
+ ---------
39
+
40
+ - `Documentation <https://cr3.github.io/pytest-xdocker/>`_
41
+ - `Release Notes <http://github.com/cr3/pytest-xdocker/blob/master/CHANGES.rst>`_
42
+ - `Issue Tracker <http://github.com/cr3/pytest-xdocker/issues>`_
43
+ - `Source Code <http://github.com/cr3/pytest-xdocker/>`_
44
+ - `PyPi <https://pypi.org/project/pytest-xdocker/>`_
@@ -0,0 +1,141 @@
1
+ [tool.poetry]
2
+ name = "pytest-xdocker"
3
+ version = "0.0.0"
4
+ description = "Pytest fixture to run docker across test runs."
5
+ authors = ["Marc Tardif"]
6
+ readme = "README.rst"
7
+ repository = "https://github.com/cr3/pytest-xdocker"
8
+ packages = [
9
+ { include = "pytest_xdocker" },
10
+ ]
11
+
12
+ [tool.poetry.dependencies]
13
+ attrs = "^24.3.0"
14
+ netifaces = "^0.11.0"
15
+ psutil = "^6.1.1"
16
+ pyhamcrest = "^2.1.0"
17
+ python = "^3.9"
18
+ pytest = "^7.4.2"
19
+ pytest-cache = "^1.0"
20
+ pytest-xprocess = "^1.0.2"
21
+
22
+ [tool.poetry.group.test.dependencies]
23
+ coverage = "^7.2.3"
24
+ pytest-unique = "^0.1.3"
25
+ yarl = "^1.18.3"
26
+
27
+ [tool.poetry.group.check]
28
+ optional = true
29
+
30
+ [tool.poetry.group.check.dependencies]
31
+ ruff = "^0.0.265"
32
+ black = "^23.3.0"
33
+ pre-commit = "^3.3.1"
34
+
35
+ [tool.poetry.group.docs]
36
+ optional = true
37
+
38
+ [tool.poetry.group.docs.dependencies]
39
+ sphinx = "^6.1.3"
40
+ sphinxcontrib-log-cabinet = "^1.0.1"
41
+ sphinx-rtd-theme = "^1.2.0"
42
+
43
+ [tool.poetry.scripts]
44
+ docker-xrun = "pytest_xdocker.docker_xrun:main"
45
+
46
+ [tool.poetry.plugins."pytest11"]
47
+ docker-xrun = "pytest_xdocker.fixtures"
48
+
49
+ [tool.poetry.plugins."pytest_unique.unique"]
50
+ ip = "pytest_xdocker.network:unique_ip"
51
+
52
+ [build-system]
53
+ requires = ["poetry-core>=1.0.0"]
54
+ build-backend = "poetry.core.masonry.api"
55
+
56
+ [tool.black]
57
+ line-length = 120
58
+ target-version = ["py39"]
59
+ preview = true
60
+
61
+ [tool.ruff]
62
+ target-version = "py39"
63
+ line-length = 120
64
+ fix = true
65
+ select = [
66
+ # flake8-2020
67
+ "YTT",
68
+ # flake8-bandit
69
+ "S",
70
+ # flake8-bugbear
71
+ "B",
72
+ # flake8-builtins
73
+ "A",
74
+ # flake8-comprehensions
75
+ "C4",
76
+ # flake8-debugger
77
+ "T10",
78
+ # flake8-simplify
79
+ "SIM",
80
+ # isort
81
+ "I",
82
+ # mccabe
83
+ "C90",
84
+ # pycodestyle
85
+ "E", "W",
86
+ # pyflakes
87
+ "F",
88
+ # pygrep-hooks
89
+ "PGH",
90
+ # pyupgrade
91
+ "UP",
92
+ # ruff
93
+ "RUF",
94
+ # tryceratops
95
+ "TRY",
96
+ ]
97
+ ignore = [
98
+ # LineTooLong
99
+ "E501",
100
+ # DoNotAssignLambda
101
+ "E731",
102
+ # Create your own exception
103
+ "TRY002",
104
+ # Avoid specifying long messages outside the exception class
105
+ "TRY003",
106
+ ]
107
+
108
+ [tool.ruff.per-file-ignores]
109
+ "tests/*" = ["S101"]
110
+
111
+ # Pytest options:
112
+ # https://docs.pytest.org/en/6.2.x/reference.html#ini-options-ref
113
+ [tool.pytest.ini_options]
114
+ addopts = [
115
+ "--doctest-modules",
116
+ "--doctest-glob=*.rst",
117
+ ]
118
+ testpaths = [
119
+ "pytest_xdocker",
120
+ "docs",
121
+ "tests",
122
+ ]
123
+
124
+ # Coverage options:
125
+ # https://coverage.readthedocs.io/en/latest/config.html
126
+ [tool.coverage.paths]
127
+ source = [
128
+ "pytest_xdocker",
129
+ "*/*/site-packages",
130
+ ]
131
+
132
+ [tool.coverage.report]
133
+ fail_under = 90
134
+ show_missing = true
135
+
136
+ [tool.coverage.run]
137
+ branch = true
138
+ parallel = true
139
+ source = [
140
+ "pytest_xdocker",
141
+ ]
File without changes
@@ -0,0 +1,98 @@
1
+ """Cache providers."""
2
+
3
+ import codecs
4
+ import json
5
+ from abc import ABCMeta, abstractmethod
6
+ from pathlib import Path
7
+
8
+ import attr
9
+
10
+
11
+ def cache_encode(data):
12
+ """Serialize cache payload."""
13
+ return json.dumps(data).encode("utf-8")
14
+
15
+
16
+ def cache_decode(payload):
17
+ """Deserialize cache payload."""
18
+ return json.loads(codecs.decode(payload, "utf-8"))
19
+
20
+
21
+ class CacheError(Exception):
22
+ """Raised with an unexpected cache error occurs."""
23
+
24
+
25
+ class Cache(metaclass=ABCMeta):
26
+ """Base class for cache providers."""
27
+
28
+ @abstractmethod
29
+ def get(self, key, default):
30
+ """Return cached value for the given key or the default."""
31
+
32
+ @abstractmethod
33
+ def set(self, key, value): # noqa: A003
34
+ """Save value for the given key."""
35
+
36
+
37
+ @attr.s(frozen=True, slots=True)
38
+ class FileCache(Cache):
39
+ """Lightweight implementation of `pytest.cache`.
40
+
41
+ :param path: Base path to cache directory.
42
+ :param encode: Encoding function, defaults to `cache_encode`
43
+ :param decode: Decoding function, defaults to `cache_decode`
44
+ """
45
+
46
+ _cachedir = attr.ib(converter=Path)
47
+ encode = attr.ib(default=cache_encode)
48
+ decode = attr.ib(default=cache_decode)
49
+
50
+ def _get_value_path(self, key):
51
+ path = self._cachedir / "v" / key
52
+ path.parent.mkdir(parents=True, exist_ok=True)
53
+ return path
54
+
55
+ def get(self, key, default):
56
+ """Read from file."""
57
+ path = self._get_value_path(key)
58
+ if path.exists():
59
+ payload = path.read_bytes()
60
+ return self.decode(payload)
61
+ else:
62
+ return default
63
+
64
+ def set(self, key, value): # noqa: A003
65
+ """Write to file."""
66
+ path = self._get_value_path(key)
67
+ payload = self.encode(value)
68
+ path.write_bytes(payload)
69
+
70
+
71
+ @attr.s(frozen=True, slots=True)
72
+ class MemoryCache(Cache):
73
+ """Memory cache."""
74
+
75
+ _memory = attr.ib(default=attr.Factory(dict))
76
+
77
+ def get(self, key, default):
78
+ """Read from dict."""
79
+ return self._memory.get(key, default)
80
+
81
+ def set(self, key, value): # noqa: A003
82
+ """Write the value to dict."""
83
+ self._memory[key] = value
84
+
85
+
86
+ @attr.s(frozen=True, slots=True)
87
+ class NullCache(Cache):
88
+ """Null cache.
89
+
90
+ This cache never sets a value and always gets the default value.
91
+ """
92
+
93
+ def get(self, key, default):
94
+ """Noop."""
95
+ return default
96
+
97
+ def set(self, key, value): # noqa: A003
98
+ """Noop."""
@@ -0,0 +1,176 @@
1
+ """Module to build shell commands declaratively.
2
+
3
+ A Command instance can be used to build a shell command:
4
+
5
+ >>> command = Command('whoami')
6
+
7
+ The command can then be executed later:
8
+
9
+ >>> lines = command.execute().splitlines()
10
+ >>> len(lines)
11
+ 1
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ import re
17
+ import shutil
18
+ import sys
19
+ from collections.abc import Iterable
20
+ from itertools import chain
21
+ from shlex import quote
22
+ from subprocess import check_output
23
+
24
+ import attr
25
+
26
+
27
+ @attr.s(eq=False, frozen=True, repr=False, slots=True)
28
+ class Command(Iterable):
29
+ """Manages a shell command."""
30
+
31
+ _command = attr.ib(converter=str)
32
+ _parent = attr.ib(default=iter(()))
33
+ _positionals = attr.ib(factory=list)
34
+ _optionals = attr.ib(factory=list)
35
+
36
+ def __eq__(self, other):
37
+ return list(self) == other
38
+
39
+ def __ne__(self, other):
40
+ return not self == other
41
+
42
+ def __iter__(self):
43
+ return chain(
44
+ self._parent,
45
+ [self._command],
46
+ self._optionals,
47
+ self._positionals,
48
+ )
49
+
50
+ def __repr__(self):
51
+ cls = self.__class__.__name__
52
+ args = ", ".join(repr(arg) for arg in self)
53
+ return f"{cls}([{args}])"
54
+
55
+ def __str__(self):
56
+ return self.to_string()
57
+
58
+ def to_string(self, escape=None):
59
+ """Stringify the command."""
60
+ if escape is None:
61
+ escape = quote
62
+
63
+ return " ".join(escape(part) for part in self)
64
+
65
+ def with_positionals(self, *positionals):
66
+ """Add positional args."""
67
+ return attr.evolve(self, positionals=self._positionals + list(positionals))
68
+
69
+ def with_optionals(self, *optionals):
70
+ """Add optional args."""
71
+ return attr.evolve(self, optionals=self._optionals + list(optionals))
72
+
73
+ def reparent(self, parent=None):
74
+ """Add a wrapping command."""
75
+ if parent is None:
76
+ parent = iter(())
77
+ return attr.evolve(self, parent=parent)
78
+
79
+ def execute(self, **kwargs):
80
+ """Run the command."""
81
+ logging.info("Executing command: %s", self)
82
+ kwargs.setdefault("universal_newlines", True)
83
+ return check_output(self, **kwargs) # noqa: S603
84
+
85
+
86
+ def empty_type():
87
+ """Option arg for an undefined optional arg."""
88
+ return args_type(min=0, max=0)
89
+
90
+
91
+ def const_type(const):
92
+ """Option arg for a constant string."""
93
+ return (const,)
94
+
95
+
96
+ def arg_type(arg, **kwargs):
97
+ """Option type for a single args."""
98
+ return args_type(arg, min=1, max=1, **kwargs)
99
+
100
+
101
+ def args_type(*args, **kwargs):
102
+ """Option type for multiple args."""
103
+ options = {
104
+ "converter": lambda arg: arg,
105
+ "min": 0,
106
+ "max": sys.maxsize,
107
+ }
108
+ options.update(kwargs)
109
+
110
+ if len(args) < options["min"]:
111
+ raise ValueError(f"Expected at least {options['min']} args, got: {args!r}")
112
+ if len(args) > options["max"]:
113
+ raise ValueError(f"Expected at most {options['max']} args, got: {args!r}")
114
+
115
+ return tuple(options["converter"](arg) for arg in args)
116
+
117
+
118
+ class OptionalArg:
119
+ """Descriptor for optional arguments.
120
+
121
+ :param name: Name of the option.
122
+ :param type: Optional argument type, defaults to `empty_type`.
123
+ :param kwargs: Optional keyword arguments passed to the type.
124
+ """
125
+
126
+ def __init__(self, name, type=empty_type, **kwargs): # noqa: A002
127
+ """Init."""
128
+ self._name = name
129
+ self._type = type
130
+ self._kwargs = kwargs
131
+
132
+ def __get__(self, obj, cls=None):
133
+ def with_func(*args):
134
+ values = self._type(*args, **self._kwargs)
135
+ return obj.with_optionals(self._name, *values)
136
+
137
+ return with_func
138
+
139
+
140
+ class PositionalArg:
141
+ """Descriptor for positional arguments.
142
+
143
+ :param type: Optional argument type, defaults to `arg_type`.
144
+ :param kwargs: Optional keyword arguments passed to the type.
145
+ """
146
+
147
+ def __init__(self, type=arg_type, **kwargs): # noqa: A002
148
+ """Init."""
149
+ self._type = type
150
+ self._kwargs = kwargs
151
+
152
+ def __get__(self, obj, cls=None):
153
+ def with_func(*args):
154
+ values = self._type(*args, **self._kwargs)
155
+ return obj.with_positionals(*values)
156
+
157
+ return with_func
158
+
159
+
160
+ def script_to_command(script, cls=Command):
161
+ """
162
+ On Windows, console scripts created as .exe should be called directly
163
+ and those created as .cmd with Python.
164
+ """
165
+ path = shutil.which(script)
166
+ if path is None:
167
+ raise OSError(f"Script not found: {script}")
168
+
169
+ base, ext = os.path.splitext(path)
170
+ if re.match(r".cmd", ext, re.IGNORECASE):
171
+ parent = Command(shutil.which("python"))
172
+ command = cls(base, parent)
173
+ else:
174
+ command = cls(path)
175
+
176
+ return command