clickqt-utils 0.0.1__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,8 @@
1
+ Copyright 2026, The copyright holders according to COPYING.md
2
+
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: clickqt-utils
3
+ Version: 0.0.1
4
+ Summary: Some click utilities that are understood by clickqt
5
+ Maintainer-email: Dominic Kempf <ssc@uni-heidelberg.de>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.md
12
+ Requires-Dist: click
13
+ Provides-Extra: tests
14
+ Requires-Dist: pytest; extra == "tests"
15
+ Requires-Dist: pytest-cov; extra == "tests"
16
+ Dynamic: license-file
17
+
18
+ # Welcome to clickqt-utils
19
+
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
21
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ssciwr/clickqt-utils/ci.yml?branch=main)](https://github.com/ssciwr/clickqt-utils/actions/workflows/ci.yml)
22
+ [![codecov](https://codecov.io/gh/ssciwr/clickqt-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/ssciwr/clickqt-utils)
23
+
24
+ A collection of `click` utilities that is also understood by `clickqt`.
25
+
26
+ ## Features
27
+
28
+ Currently, `clickqt-utils` contains the following utilities:
29
+
30
+ * `PathWithExtensions`: a `click.Path` type that only accepts files with configured extensions.
31
+
32
+ They are usable with any CLI written in `click`, but they have the advantage
33
+ of being directly understood by `clickqt`. Notably, this package does not
34
+ depend on `clickqt`, so depending on this will not make you depend on QT,
35
+ but your users can opt into using `clickqt` for GUI support of your tools.
36
+
37
+ ## Installation
38
+
39
+ The Python package `clickqt-utils` can be installed from PyPI:
40
+
41
+ ```
42
+ python -m pip install clickqt-utils
43
+ ```
44
+
45
+ ## Acknowledgments
46
+
47
+ This repository was set up using the [SSC Cookiecutter for Python Packages](https://github.com/ssciwr/cookiecutter-python-package).
@@ -0,0 +1,30 @@
1
+ # Welcome to clickqt-utils
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ssciwr/clickqt-utils/ci.yml?branch=main)](https://github.com/ssciwr/clickqt-utils/actions/workflows/ci.yml)
5
+ [![codecov](https://codecov.io/gh/ssciwr/clickqt-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/ssciwr/clickqt-utils)
6
+
7
+ A collection of `click` utilities that is also understood by `clickqt`.
8
+
9
+ ## Features
10
+
11
+ Currently, `clickqt-utils` contains the following utilities:
12
+
13
+ * `PathWithExtensions`: a `click.Path` type that only accepts files with configured extensions.
14
+
15
+ They are usable with any CLI written in `click`, but they have the advantage
16
+ of being directly understood by `clickqt`. Notably, this package does not
17
+ depend on `clickqt`, so depending on this will not make you depend on QT,
18
+ but your users can opt into using `clickqt` for GUI support of your tools.
19
+
20
+ ## Installation
21
+
22
+ The Python package `clickqt-utils` can be installed from PyPI:
23
+
24
+ ```
25
+ python -m pip install clickqt-utils
26
+ ```
27
+
28
+ ## Acknowledgments
29
+
30
+ This repository was set up using the [SSC Cookiecutter for Python Packages](https://github.com/ssciwr/cookiecutter-python-package).
@@ -0,0 +1,13 @@
1
+ from importlib import metadata
2
+
3
+ __version__ = metadata.version(__package__)
4
+ del metadata
5
+
6
+
7
+ def add_one(x: int):
8
+ """An example function that increases a number
9
+
10
+ :param x: The input parameter to increase
11
+ :return: The successor of the given number
12
+ """
13
+ return x + 1
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Iterable, Mapping
5
+ from typing import Callable, Any
6
+
7
+ import click
8
+
9
+
10
+ class PathWithExtensions(click.Path):
11
+ """A ``click.Path`` variant that restricts accepted file extensions."""
12
+
13
+ def __init__(
14
+ self,
15
+ *args: Any,
16
+ file_extensions: Iterable[str] | Mapping[Iterable[str], str] | None = None,
17
+ **kwargs: Any,
18
+ ) -> None:
19
+ super().__init__(*args, **kwargs)
20
+
21
+ self.file_extension_groups = self._normalize_extension_groups(file_extensions)
22
+ self.file_extensions = tuple(
23
+ dict.fromkeys(
24
+ extension
25
+ for extension_group, _group_name in self.file_extension_groups
26
+ for extension in extension_group
27
+ )
28
+ )
29
+
30
+ self.validation_callbacks: tuple[
31
+ Callable[
32
+ [
33
+ str | bytes | os.PathLike[str] | os.PathLike[bytes],
34
+ click.Parameter | None,
35
+ click.Context | None,
36
+ ],
37
+ None,
38
+ ],
39
+ ...,
40
+ ] = (
41
+ self._validate_not_directory,
42
+ self._validate_file_extension,
43
+ )
44
+
45
+ def convert(
46
+ self,
47
+ value: str | bytes | os.PathLike[str] | os.PathLike[bytes],
48
+ param: click.Parameter | None,
49
+ ctx: click.Context | None,
50
+ ) -> str | bytes | os.PathLike[str] | os.PathLike[bytes]:
51
+ converted = super().convert(value, param, ctx)
52
+
53
+ for validation_callback in self.validation_callbacks:
54
+ validation_callback(converted, param, ctx)
55
+
56
+ return converted
57
+
58
+ def _validate_not_directory(
59
+ self,
60
+ value: str | bytes | os.PathLike[str] | os.PathLike[bytes],
61
+ param: click.Parameter | None,
62
+ ctx: click.Context | None,
63
+ ) -> None:
64
+ path = os.fspath(value)
65
+ if os.path.isdir(path):
66
+ self.fail(f"{path!r} is a directory.", param, ctx)
67
+
68
+ def _validate_file_extension(
69
+ self,
70
+ value: str | bytes | os.PathLike[str] | os.PathLike[bytes],
71
+ param: click.Parameter | None,
72
+ ctx: click.Context | None,
73
+ ) -> None:
74
+ if not self.file_extensions:
75
+ return
76
+
77
+ path = os.fspath(value)
78
+ lower_path = os.fsdecode(path).lower()
79
+
80
+ if any(lower_path.endswith(extension) for extension in self.file_extensions):
81
+ return
82
+
83
+ allowed_extensions = ", ".join(self.file_extensions)
84
+ self.fail(
85
+ f"{path!r} does not have one of the required extensions: {allowed_extensions}",
86
+ param,
87
+ ctx,
88
+ )
89
+
90
+ def _normalize_extension_groups(
91
+ self,
92
+ file_extensions: Iterable[str] | Mapping[Iterable[str], str] | None,
93
+ ) -> tuple[tuple[tuple[str, ...], str], ...]:
94
+ if file_extensions is None:
95
+ return ()
96
+
97
+ if isinstance(file_extensions, Mapping):
98
+ return tuple(
99
+ (self._normalize_extensions(extensions), group_name)
100
+ for extensions, group_name in file_extensions.items()
101
+ )
102
+
103
+ normalized_extensions = self._normalize_extensions(file_extensions)
104
+ if not normalized_extensions:
105
+ return ()
106
+ return ((normalized_extensions, "Allowed Files"),)
107
+
108
+ @staticmethod
109
+ def _normalize_extensions(file_extensions: Iterable[str]) -> tuple[str, ...]:
110
+ return tuple(
111
+ extension.lower() if extension.startswith(".") else f".{extension.lower()}"
112
+ for extension in file_extensions
113
+ )
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: clickqt-utils
3
+ Version: 0.0.1
4
+ Summary: Some click utilities that are understood by clickqt
5
+ Maintainer-email: Dominic Kempf <ssc@uni-heidelberg.de>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.md
12
+ Requires-Dist: click
13
+ Provides-Extra: tests
14
+ Requires-Dist: pytest; extra == "tests"
15
+ Requires-Dist: pytest-cov; extra == "tests"
16
+ Dynamic: license-file
17
+
18
+ # Welcome to clickqt-utils
19
+
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
21
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ssciwr/clickqt-utils/ci.yml?branch=main)](https://github.com/ssciwr/clickqt-utils/actions/workflows/ci.yml)
22
+ [![codecov](https://codecov.io/gh/ssciwr/clickqt-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/ssciwr/clickqt-utils)
23
+
24
+ A collection of `click` utilities that is also understood by `clickqt`.
25
+
26
+ ## Features
27
+
28
+ Currently, `clickqt-utils` contains the following utilities:
29
+
30
+ * `PathWithExtensions`: a `click.Path` type that only accepts files with configured extensions.
31
+
32
+ They are usable with any CLI written in `click`, but they have the advantage
33
+ of being directly understood by `clickqt`. Notably, this package does not
34
+ depend on `clickqt`, so depending on this will not make you depend on QT,
35
+ but your users can opt into using `clickqt` for GUI support of your tools.
36
+
37
+ ## Installation
38
+
39
+ The Python package `clickqt-utils` can be installed from PyPI:
40
+
41
+ ```
42
+ python -m pip install clickqt-utils
43
+ ```
44
+
45
+ ## Acknowledgments
46
+
47
+ This repository was set up using the [SSC Cookiecutter for Python Packages](https://github.com/ssciwr/cookiecutter-python-package).
@@ -0,0 +1,11 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ clickqt_utils/__init__.py
5
+ clickqt_utils/extensions.py
6
+ clickqt_utils.egg-info/PKG-INFO
7
+ clickqt_utils.egg-info/SOURCES.txt
8
+ clickqt_utils.egg-info/dependency_links.txt
9
+ clickqt_utils.egg-info/requires.txt
10
+ clickqt_utils.egg-info/top_level.txt
11
+ tests/test_extensions.py
@@ -0,0 +1,5 @@
1
+ click
2
+
3
+ [tests]
4
+ pytest
5
+ pytest-cov
@@ -0,0 +1 @@
1
+ clickqt_utils
@@ -0,0 +1,50 @@
1
+ # This section describes the requirements of the build/installation
2
+ # process itself. Being able to do this was the original reason to
3
+ # introduce pyproject.toml
4
+ [build-system]
5
+ requires = [
6
+ "setuptools >=61",
7
+ ]
8
+ build-backend = "setuptools.build_meta"
9
+
10
+ # This section provides general project metadata that is used across
11
+ # a variety of build tools. Notably, the version specified here is the
12
+ # single source of truth for clickqt_utils's version
13
+ [project]
14
+ name = "clickqt-utils"
15
+ description = "Some click utilities that are understood by clickqt"
16
+ readme = "README.md"
17
+ maintainers = [
18
+ { name = "Dominic Kempf", email = "ssc@uni-heidelberg.de" },
19
+ ]
20
+ version = "0.0.1"
21
+ requires-python = ">=3.10"
22
+ license-files = ["LICENSE.md"]
23
+ license = "MIT"
24
+ classifiers = [
25
+ "Programming Language :: Python :: 3",
26
+ "Operating System :: OS Independent",
27
+ ]
28
+ dependencies = [
29
+ "click",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ tests = [
34
+ "pytest",
35
+ "pytest-cov",
36
+ ]
37
+
38
+
39
+ # The following section contains setuptools-specific configuration
40
+ # options. For a full reference of available options, check the overview
41
+ # at https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
42
+ [tool.setuptools.packages.find]
43
+ where = ["."]
44
+ include = ["clickqt_utils*"]
45
+
46
+ # The following is the configuration for the pytest test suite
47
+ [tool.pytest.ini_options]
48
+ testpaths = [
49
+ "tests",
50
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,135 @@
1
+ import click
2
+ import pytest
3
+
4
+ from click.testing import CliRunner
5
+ from clickqt_utils.extensions import PathWithExtensions
6
+
7
+
8
+ def test_path_with_extensions_stores_file_extensions():
9
+ path_type = PathWithExtensions(file_extensions=["txt", ".csv"])
10
+
11
+ assert path_type.file_extensions == (".txt", ".csv")
12
+ assert path_type.file_extension_groups == (((".txt", ".csv"), "Allowed Files"),)
13
+ assert path_type.validation_callbacks[1] == path_type._validate_file_extension
14
+
15
+
16
+ def test_path_with_extensions_stores_extension_groups_for_dialog():
17
+ path_type = PathWithExtensions(
18
+ file_extensions={
19
+ ("png", ".JPEG"): "Images",
20
+ ("txt",): "Text Files",
21
+ }
22
+ )
23
+
24
+ assert path_type.file_extensions == (".png", ".jpeg", ".txt")
25
+ assert path_type.file_extension_groups == (
26
+ ((".png", ".jpeg"), "Images"),
27
+ ((".txt",), "Text Files"),
28
+ )
29
+
30
+
31
+ def test_path_with_extensions_accepts_matching_extension(tmp_path):
32
+ input_file = tmp_path / "input.TXT"
33
+ input_file.write_text("data", encoding="utf-8")
34
+
35
+ path_type = PathWithExtensions(file_extensions=[".txt"], exists=True)
36
+
37
+ assert path_type.convert(str(input_file), None, None) == str(input_file)
38
+
39
+
40
+ def test_path_with_extensions_rejects_wrong_extension(tmp_path):
41
+ input_file = tmp_path / "input.csv"
42
+ input_file.write_text("data", encoding="utf-8")
43
+
44
+ path_type = PathWithExtensions(file_extensions=["txt"], exists=True)
45
+
46
+ with pytest.raises(click.BadParameter, match="required extensions"):
47
+ path_type.convert(str(input_file), None, None)
48
+
49
+
50
+ def test_path_with_extensions_accepts_grouped_extensions(tmp_path):
51
+ image_file = tmp_path / "photo.JPEG"
52
+ image_file.write_text("img", encoding="utf-8")
53
+ text_file = tmp_path / "readme.txt"
54
+ text_file.write_text("txt", encoding="utf-8")
55
+
56
+ path_type = PathWithExtensions(
57
+ file_extensions={
58
+ ("png", ".jpeg"): "Images",
59
+ ("txt",): "Text Files",
60
+ },
61
+ exists=True,
62
+ )
63
+
64
+ assert path_type.convert(str(image_file), None, None) == str(image_file)
65
+ assert path_type.convert(str(text_file), None, None) == str(text_file)
66
+
67
+
68
+ def test_path_with_extensions_rejects_directories(tmp_path):
69
+ input_dir = tmp_path / "folder.txt"
70
+ input_dir.mkdir()
71
+
72
+ path_type = PathWithExtensions(file_extensions=[".txt"], exists=True)
73
+
74
+ with pytest.raises(click.BadParameter, match="directory"):
75
+ path_type.convert(str(input_dir), None, None)
76
+
77
+
78
+ @click.command()
79
+ @click.argument(
80
+ "path",
81
+ type=PathWithExtensions(file_extensions=[".txt"], exists=True),
82
+ )
83
+ def cli(path):
84
+ click.echo(path)
85
+
86
+
87
+ def test_path_with_extensions_click_cli_system(tmp_path):
88
+ runner = CliRunner()
89
+
90
+ valid_file = tmp_path / "valid.txt"
91
+ valid_file.write_text("ok", encoding="utf-8")
92
+ valid_result = runner.invoke(cli, [str(valid_file)])
93
+ assert valid_result.exit_code == 0
94
+ assert str(valid_file) in valid_result.output
95
+
96
+ invalid_file = tmp_path / "invalid.csv"
97
+ invalid_file.write_text("no", encoding="utf-8")
98
+ invalid_result = runner.invoke(cli, [str(invalid_file)])
99
+ assert invalid_result.exit_code != 0
100
+ assert "required extensions" in invalid_result.output
101
+
102
+ invalid_dir = tmp_path / "folder.txt"
103
+ invalid_dir.mkdir()
104
+ dir_result = runner.invoke(cli, [str(invalid_dir)])
105
+ assert dir_result.exit_code != 0
106
+ assert "is a directory" in dir_result.output
107
+
108
+
109
+ def test_path_with_extensions_grouped_extensions_click_cli_system(tmp_path):
110
+ @click.command()
111
+ @click.argument(
112
+ "path",
113
+ type=PathWithExtensions(
114
+ file_extensions={
115
+ ("png", ".jpeg"): "Images",
116
+ ("txt",): "Text Files",
117
+ },
118
+ exists=True,
119
+ ),
120
+ )
121
+ def grouped_cli(path):
122
+ click.echo(path)
123
+
124
+ runner = CliRunner()
125
+
126
+ image_file = tmp_path / "photo.png"
127
+ image_file.write_text("image", encoding="utf-8")
128
+ image_result = runner.invoke(grouped_cli, [str(image_file)])
129
+ assert image_result.exit_code == 0
130
+
131
+ invalid_file = tmp_path / "archive.zip"
132
+ invalid_file.write_text("zip", encoding="utf-8")
133
+ invalid_result = runner.invoke(grouped_cli, [str(invalid_file)])
134
+ assert invalid_result.exit_code != 0
135
+ assert "required extensions" in invalid_result.output