fotolab 0.31.1__tar.gz → 0.31.4__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.
Files changed (57) hide show
  1. fotolab-0.31.4/.pre-commit-config.yaml +120 -0
  2. {fotolab-0.31.1 → fotolab-0.31.4}/CHANGELOG.md +17 -0
  3. {fotolab-0.31.1 → fotolab-0.31.4}/CONTRIBUTING.md +3 -3
  4. {fotolab-0.31.1 → fotolab-0.31.4}/PKG-INFO +7 -12
  5. {fotolab-0.31.1 → fotolab-0.31.4}/README.md +4 -10
  6. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/__init__.py +2 -1
  7. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/__init__.py +1 -2
  8. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/animate.py +1 -4
  9. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/border.py +2 -8
  10. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/contrast.py +3 -9
  11. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/halftone.py +2 -6
  12. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/info.py +1 -3
  13. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/montage.py +1 -3
  14. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/resize.py +1 -4
  15. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/watermark.py +5 -17
  16. {fotolab-0.31.1 → fotolab-0.31.4}/noxfile.py +64 -84
  17. {fotolab-0.31.1 → fotolab-0.31.4}/pyproject.toml +26 -1
  18. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_animate_subcommand.py +2 -1
  19. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_auto_subcommand.py +1 -0
  20. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_border_subcommand.py +2 -1
  21. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_contrast_subcommand.py +2 -1
  22. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_halftone_subcommand.py +2 -1
  23. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_info_subcommand.py +2 -1
  24. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_montage_subcommand.py +2 -1
  25. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_resize_subcommand.py +2 -1
  26. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_rotate_subcommand.py +2 -1
  27. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_sharpen_subcommand.py +2 -1
  28. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_watermark_subcommand.py +2 -1
  29. fotolab-0.31.4/uv.lock +1377 -0
  30. fotolab-0.31.1/.pre-commit-config.yaml +0 -113
  31. fotolab-0.31.1/Pipfile +0 -32
  32. fotolab-0.31.1/Pipfile.lock +0 -1064
  33. {fotolab-0.31.1 → fotolab-0.31.4}/.coveragerc +0 -0
  34. {fotolab-0.31.1 → fotolab-0.31.4}/.gitignore +0 -0
  35. {fotolab-0.31.1 → fotolab-0.31.4}/.python-version +0 -0
  36. {fotolab-0.31.1 → fotolab-0.31.4}/LICENSE.md +0 -0
  37. {fotolab-0.31.1 → fotolab-0.31.4}/docs/Makefile +0 -0
  38. {fotolab-0.31.1 → fotolab-0.31.4}/docs/make.bat +0 -0
  39. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/CHANGELOG.md +0 -0
  40. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/CONTRIBUTING.md +0 -0
  41. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/LICENSE.md +0 -0
  42. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/README.md +0 -0
  43. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/_static/logo.jpg +0 -0
  44. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/conf.py +0 -0
  45. {fotolab-0.31.1 → fotolab-0.31.4}/docs/source/index.rst +0 -0
  46. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/__main__.py +0 -0
  47. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/cli.py +0 -0
  48. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/auto.py +0 -0
  49. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/env.py +0 -0
  50. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/rotate.py +0 -0
  51. {fotolab-0.31.1 → fotolab-0.31.4}/fotolab/subcommands/sharpen.py +0 -0
  52. {fotolab-0.31.1 → fotolab-0.31.4}/generate +0 -0
  53. {fotolab-0.31.1 → fotolab-0.31.4}/tests/__init__.py +0 -0
  54. {fotolab-0.31.1 → fotolab-0.31.4}/tests/conftest.py +0 -0
  55. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_env_subcommand.py +0 -0
  56. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_help_flag.py +0 -0
  57. {fotolab-0.31.1 → fotolab-0.31.4}/tests/test_quiet_flag.py +0 -0
@@ -0,0 +1,120 @@
1
+ # See https://pre-commit.com for more information
2
+ # See https://pre-commit.com/hooks.html for more hooks
3
+ repos:
4
+ - repo: https://github.com/pre-commit/pre-commit-hooks
5
+ rev: v5.0.0
6
+ hooks:
7
+ - id: check-case-conflict
8
+ - id: check-merge-conflict
9
+ - id: check-toml
10
+ - id: check-yaml
11
+ - id: debug-statements
12
+ - id: detect-private-key
13
+ - id: end-of-file-fixer
14
+ - id: mixed-line-ending
15
+ - id: trailing-whitespace
16
+
17
+ - repo: https://github.com/abravalheri/validate-pyproject
18
+ rev: v0.24.1
19
+ hooks:
20
+ - id: validate-pyproject
21
+ name: validate-pyproject
22
+
23
+ - repo: https://github.com/codespell-project/codespell
24
+ rev: v2.4.1
25
+ hooks:
26
+ - id: codespell
27
+ args:
28
+ - --ignore-words-list=astroid
29
+
30
+ - repo: https://github.com/pre-commit/mirrors-prettier
31
+ rev: v4.0.0-alpha.8
32
+ hooks:
33
+ - id: prettier
34
+ exclude: (Pipfile.lock)
35
+
36
+ - repo: https://github.com/astral-sh/ruff-pre-commit
37
+ rev: v0.11.13
38
+ hooks:
39
+ - id: ruff-check
40
+ args: [--fix]
41
+ - id: ruff-format
42
+
43
+ # - repo: https://github.com/pycqa/isort
44
+ # rev: 6.0.1
45
+ # hooks:
46
+ # - id: isort
47
+ # additional_dependencies:
48
+ # - isort[pyproject]
49
+ # args:
50
+ # - --profile=black
51
+ # - --line-length=79
52
+ # - --py=312
53
+
54
+ # - repo: https://github.com/psf/black
55
+ # rev: 25.1.0
56
+ # hooks:
57
+ # - id: black
58
+ # language_version: python3.13
59
+ # args:
60
+ # - --line-length=79
61
+ # - --target-version=py38
62
+ # - --target-version=py39
63
+ # - --target-version=py310
64
+ # - --target-version=py311
65
+ # - --target-version=py312
66
+
67
+ # - repo: https://github.com/asottile/blacken-docs
68
+ # rev: 1.19.1
69
+ # hooks:
70
+ # - id: blacken-docs
71
+ # additional_dependencies:
72
+ # - black==22.8.0
73
+
74
+ # - repo: https://github.com/PyCQA/autoflake
75
+ # rev: v2.3.1
76
+ # hooks:
77
+ # - id: autoflake
78
+ # args:
79
+ # - --in-place
80
+ # - --remove-unused-variables
81
+ # - --remove-all-unused-imports
82
+ # language: python
83
+ # files: \.py$
84
+ # language_version: python3.13
85
+
86
+ # - repo: https://github.com/PyCQA/flake8
87
+ # rev: 7.2.0
88
+ # hooks:
89
+ # - id: flake8
90
+ # language_version: python3.13
91
+ # additional_dependencies:
92
+ # - flake8-docstrings
93
+ # - flake8-pytest-style
94
+ # args:
95
+ # - --docstring-convention=google
96
+ # - --show-source
97
+ # - --max-line-length=79
98
+ # - --exclude=docs/source/conf.py
99
+ # - --per-file-ignores=fotolab/*:D208 tests/*:D100,D103,D104,E501
100
+
101
+ # - repo: local
102
+ # hooks:
103
+ # - id: pylint
104
+ # name: pylint
105
+ # entry: pylint
106
+ # language: system
107
+ # types:
108
+ # - python
109
+ # exclude: docs/
110
+ # args:
111
+ # - fotolab
112
+ # - tests
113
+ # - --unsafe-load-any-extension=y
114
+ # - --disable=R0801,W0212
115
+
116
+ - repo: https://github.com/pre-commit/mirrors-mypy
117
+ rev: v1.16.0
118
+ hooks:
119
+ - id: mypy
120
+ exclude: docs/
@@ -7,6 +7,23 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.31.4 (2025-06-15)
11
+
12
+ - Fix `deps` job in `nox` session due to migration to `uv`
13
+ - Fix `pre-commit` not available in `lint` session
14
+ - Initial migration from `pipenv` to `uv`
15
+ - Refactor release `nox` job to use uv version
16
+ - Resolve `uv` not using nox's `venv` warning
17
+
18
+ ## v0.31.2 (2025-06-08)
19
+
20
+ - Add `ruff` to dev deps
21
+ - Add `ruff` to `pre-commit` hook
22
+ - Bump `pre-commit` hook for mypy
23
+ - Code format
24
+ - Skip `pre-commit` manual installation
25
+ - Sort deps
26
+
10
27
  ## v0.31.1 (2025-06-01)
11
28
 
12
29
  - Code format
@@ -31,14 +31,14 @@ pyenv install $(cat .python-version)
31
31
  Install necessary packages:
32
32
 
33
33
  ```console
34
- python -m pip install --upgrade pip pipenv nox
35
- pipenv install --dev
34
+ python -m pip install --upgrade pip uv nox
35
+ uv pip install -e .[dev]
36
36
  ```
37
37
 
38
38
  Spawn a shell in virtual environment for your development:
39
39
 
40
40
  ```console
41
- pipenv shell
41
+ uv shell
42
42
  ```
43
43
 
44
44
  Show all available `nox` sessions:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: fotolab
3
- Version: 0.31.1
3
+ Version: 0.31.4
4
4
  Summary: A console program that manipulate images.
5
5
  Keywords: photography,photo
6
6
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python
19
+ License-File: LICENSE.md
19
20
  Requires-Dist: pillow
20
21
  Project-URL: Changelog, https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md
21
22
  Project-URL: Issues, https://github.com/kianmeng/fotolab/issues
@@ -27,28 +28,22 @@ A console program to manipulate photos.
27
28
 
28
29
  ## Installation
29
30
 
30
- Stable version From PyPI using `pipx`:
31
+ Stable version From PyPI using `uv`:
31
32
 
32
33
  ```console
33
- pipx install fotolab
34
- ```
35
-
36
- Stable version From PyPI using `pip`:
37
-
38
- ```console
39
- python -m pip install fotolab
34
+ uv pip install fotolab
40
35
  ```
41
36
 
42
37
  Upgrade to latest stable version:
43
38
 
44
39
  ```console
45
- python3 -m pip install fotolab --upgrade
40
+ uv pip install fotolab --upgrade
46
41
  ```
47
42
 
48
43
  Latest development version from GitHub:
49
44
 
50
45
  ```console
51
- python -m pip install -e git+https://github.com/kianmeng/fotolab.git
46
+ uv pip install -e git+https://github.com/kianmeng/fotolab.git
52
47
  ```
53
48
 
54
49
  ## Usage
@@ -4,28 +4,22 @@ A console program to manipulate photos.
4
4
 
5
5
  ## Installation
6
6
 
7
- Stable version From PyPI using `pipx`:
7
+ Stable version From PyPI using `uv`:
8
8
 
9
9
  ```console
10
- pipx install fotolab
11
- ```
12
-
13
- Stable version From PyPI using `pip`:
14
-
15
- ```console
16
- python -m pip install fotolab
10
+ uv pip install fotolab
17
11
  ```
18
12
 
19
13
  Upgrade to latest stable version:
20
14
 
21
15
  ```console
22
- python3 -m pip install fotolab --upgrade
16
+ uv pip install fotolab --upgrade
23
17
  ```
24
18
 
25
19
  Latest development version from GitHub:
26
20
 
27
21
  ```console
28
- python -m pip install -e git+https://github.com/kianmeng/fotolab.git
22
+ uv pip install -e git+https://github.com/kianmeng/fotolab.git
29
23
  ```
30
24
 
31
25
  ## Usage
@@ -20,11 +20,12 @@ import logging
20
20
  import os
21
21
  import subprocess
22
22
  import sys
23
+ from importlib import metadata
23
24
  from pathlib import Path
24
25
 
25
26
  from PIL import Image
26
27
 
27
- __version__ = "0.31.1"
28
+ __version__ = metadata.version("fotolab")
28
29
 
29
30
  log = logging.getLogger(__name__)
30
31
 
@@ -24,8 +24,7 @@ def build_subparser(subparsers):
24
24
  iter_namespace = pkgutil.iter_modules(__path__, __name__ + ".")
25
25
 
26
26
  subcommands = {
27
- name: importlib.import_module(name)
28
- for finder, name, ispkg in iter_namespace
27
+ name: importlib.import_module(name) for finder, name, ispkg in iter_namespace
29
28
  }
30
29
 
31
30
  for subcommand in subcommands.values():
@@ -123,10 +123,7 @@ def build_subparser(subparsers) -> None:
123
123
  type=int,
124
124
  default=4,
125
125
  choices=range(0, 7),
126
- help=(
127
- "set WEBP encoding method "
128
- "(0=fast, 6=slow/best, default: '%(default)s')"
129
- ),
126
+ help=("set WEBP encoding method (0=fast, 6=slow/best, default: '%(default)s')"),
130
127
  metavar="METHOD",
131
128
  )
132
129
 
@@ -77,10 +77,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
77
77
  dest="width_right",
78
78
  type=int,
79
79
  default=0,
80
- help=(
81
- "set the width of right border in pixels"
82
- " (default: '%(default)s')"
83
- ),
80
+ help=("set the width of right border in pixels (default: '%(default)s')"),
84
81
  metavar="WIDTH",
85
82
  )
86
83
 
@@ -90,10 +87,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
90
87
  dest="width_bottom",
91
88
  type=int,
92
89
  default=0,
93
- help=(
94
- "set the width of bottom border in pixels"
95
- " (default: '%(default)s')"
96
- ),
90
+ help=("set the width of bottom border in pixels (default: '%(default)s')"),
97
91
  metavar="WIDTH",
98
92
  )
99
93
 
@@ -31,9 +31,7 @@ def _validate_cutoff(value: str) -> float:
31
31
  try:
32
32
  f_value = float(value)
33
33
  except ValueError as e:
34
- raise argparse.ArgumentTypeError(
35
- f"invalid float value: '{value}'"
36
- ) from e
34
+ raise argparse.ArgumentTypeError(f"invalid float value: '{value}'") from e
37
35
  if not 0 <= f_value <= 50:
38
36
  raise argparse.ArgumentTypeError(
39
37
  f"cutoff value {f_value} must be between 0 and 50"
@@ -43,9 +41,7 @@ def _validate_cutoff(value: str) -> float:
43
41
 
44
42
  def build_subparser(subparsers: argparse._SubParsersAction) -> None:
45
43
  """Build the subparser."""
46
- contrast_parser = subparsers.add_parser(
47
- "contrast", help="contrast an image."
48
- )
44
+ contrast_parser = subparsers.add_parser("contrast", help="contrast an image.")
49
45
 
50
46
  contrast_parser.set_defaults(func=run)
51
47
 
@@ -103,8 +99,6 @@ def run(args: argparse.Namespace) -> None:
103
99
 
104
100
  for image_filename in args.image_filenames:
105
101
  original_image = Image.open(image_filename)
106
- contrast_image = ImageOps.autocontrast(
107
- original_image, cutoff=args.cutoff
108
- )
102
+ contrast_image = ImageOps.autocontrast(original_image, cutoff=args.cutoff)
109
103
 
110
104
  save_image(args, contrast_image, image_filename, "contrast")
@@ -37,9 +37,7 @@ class HalftoneCell(NamedTuple):
37
37
 
38
38
  def build_subparser(subparsers) -> None:
39
39
  """Build the subparser."""
40
- halftone_parser = subparsers.add_parser(
41
- "halftone", help="halftone an image"
42
- )
40
+ halftone_parser = subparsers.add_parser("halftone", help="halftone an image")
43
41
 
44
42
  halftone_parser.set_defaults(func=run)
45
43
 
@@ -156,9 +154,7 @@ def _draw_halftone_dot(
156
154
  else:
157
155
  # Calculate brightness (luminance) from the RGB color
158
156
  brightness = int(
159
- 0.299 * pixel_value[0]
160
- + 0.587 * pixel_value[1]
161
- + 0.114 * pixel_value[2]
157
+ 0.299 * pixel_value[0] + 0.587 * pixel_value[1] + 0.114 * pixel_value[2]
162
158
  )
163
159
  dot_fill = pixel_value # Use original color for color dots
164
160
 
@@ -107,9 +107,7 @@ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
107
107
  if exif:
108
108
  info = {ExifTags.TAGS.get(tag_id): exif.get(tag_id) for tag_id in exif}
109
109
 
110
- filtered_info = {
111
- key: value for key, value in info.items() if key is not None
112
- }
110
+ filtered_info = {key: value for key, value in info.items() if key is not None}
113
111
  if sort:
114
112
  filtered_info = dict(sorted(filtered_info.items()))
115
113
 
@@ -27,9 +27,7 @@ log = logging.getLogger(__name__)
27
27
 
28
28
  def build_subparser(subparsers) -> None:
29
29
  """Build the subparser."""
30
- montage_parser = subparsers.add_parser(
31
- "montage", help="montage a list of image"
32
- )
30
+ montage_parser = subparsers.add_parser("montage", help="montage a list of image")
33
31
 
34
32
  montage_parser.set_defaults(func=run)
35
33
 
@@ -59,10 +59,7 @@ def build_subparser(subparsers) -> None:
59
59
  "--canvas-color",
60
60
  default="black",
61
61
  dest="canvas_color",
62
- help=(
63
- "the color of the extended larger canvas"
64
- "(default: '%(default)s')"
65
- ),
62
+ help=("the color of the extended larger canvas(default: '%(default)s')"),
66
63
  )
67
64
 
68
65
  if "-c" in sys.argv or "--canvas" in sys.argv:
@@ -109,10 +109,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
109
109
  dest="outline_width",
110
110
  type=int,
111
111
  default=2,
112
- help=(
113
- "set the outline width of the watermark text "
114
- "(default: '%(default)s')"
115
- ),
112
+ help=("set the outline width of the watermark text (default: '%(default)s')"),
116
113
  metavar="OUTLINE_WIDTH",
117
114
  )
118
115
 
@@ -122,10 +119,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
122
119
  dest="outline_color",
123
120
  type=str,
124
121
  default="black",
125
- help=(
126
- "set the outline color of the watermark text "
127
- "(default: '%(default)s')"
128
- ),
122
+ help=("set the outline color of the watermark text (default: '%(default)s')"),
129
123
  metavar="OUTLINE_COLOR",
130
124
  )
131
125
 
@@ -203,9 +197,7 @@ def run(args: argparse.Namespace) -> None:
203
197
  if image.format == "GIF":
204
198
  watermark_gif_image(image, image_filename, args)
205
199
  else:
206
- watermarked_image: Image.Image = watermark_non_gif_image(
207
- image, args
208
- )
200
+ watermarked_image: Image.Image = watermark_non_gif_image(image, args)
209
201
  save_image(args, watermarked_image, image_filename, "watermark")
210
202
 
211
203
 
@@ -341,16 +333,12 @@ def calc_font_size(image: Image.Image, args: argparse.Namespace) -> int:
341
333
  return new_font_size
342
334
 
343
335
 
344
- def calc_font_outline_width(
345
- image: Image.Image, args: argparse.Namespace
346
- ) -> int:
336
+ def calc_font_outline_width(image: Image.Image, args: argparse.Namespace) -> int:
347
337
  """Calculate the font padding based on the width of the image."""
348
338
  width, _height = image.size
349
339
  new_font_outline_width: int = args.outline_width
350
340
  if width > 600:
351
- new_font_outline_width = math.floor(
352
- FONT_OUTLINE_WIDTH_ASPECT_RATIO * width
353
- )
341
+ new_font_outline_width = math.floor(FONT_OUTLINE_WIDTH_ASPECT_RATIO * width)
354
342
 
355
343
  log.debug("new font outline width: %d", new_font_outline_width)
356
344
  return new_font_outline_width
@@ -17,19 +17,17 @@
17
17
 
18
18
  """Nox configuration."""
19
19
 
20
- import ast
20
+ import argparse
21
21
  import datetime
22
22
 
23
23
  import nox
24
- from packaging.version import Version
25
24
 
26
25
 
27
26
  @nox.session(python="3.9")
28
27
  def deps(session: nox.Session) -> None:
29
28
  """Update pre-commit hooks and deps."""
30
- session.install("pre-commit", "pipenv")
29
+ _uv_install(session)
31
30
  session.run("pre-commit", "autoupdate", *session.posargs)
32
- session.run("pipenv", "update", env={"PIPENV_VERBOSITY": "-1"})
33
31
 
34
32
 
35
33
  @nox.session(python="3.13")
@@ -40,25 +38,23 @@ def lint(session: nox.Session) -> None:
40
38
 
41
39
  nox -s lint -- pylint
42
40
  """
43
- session.install("pre-commit")
41
+ _uv_install(session)
44
42
  session.run("pre-commit", "run", "--all-files", *session.posargs)
45
43
 
46
44
 
47
45
  @nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13"])
48
46
  def test(session: nox.Session) -> None:
49
47
  """Run test."""
50
- _pipenv_install(session)
51
- session.run(
52
- "pipenv", "run", "pytest", "--numprocesses", "auto", *session.posargs
53
- )
48
+ _uv_install(session)
49
+ session.run("uv", "run", "pytest", "--numprocesses", "auto", *session.posargs)
54
50
 
55
51
 
56
52
  @nox.session(python="3.13")
57
53
  def cov(session: nox.Session) -> None:
58
54
  """Run test coverage."""
59
- _pipenv_install(session)
55
+ _uv_install(session)
60
56
  session.run(
61
- "pipenv",
57
+ "uv",
62
58
  "run",
63
59
  "pytest",
64
60
  "--numprocesses",
@@ -73,18 +69,16 @@ def cov(session: nox.Session) -> None:
73
69
  @nox.session(python="3.13")
74
70
  def doc(session: nox.Session) -> None:
75
71
  """Build doc with sphinx."""
76
- _pipenv_install(session)
77
- session.run(
78
- "sphinx-build", "docs/source/", "docs/build/html", *session.posargs
79
- )
72
+ _uv_install(session)
73
+ session.run("sphinx-build", "docs/source/", "docs/build/html", *session.posargs)
80
74
 
81
75
 
82
76
  @nox.session(python="3.13")
83
77
  def readme(session: nox.Session) -> None:
84
78
  """Update console help menu to readme."""
85
- _pipenv_install(session)
79
+ _uv_install(session)
86
80
  with open("README.md", "r+", encoding="utf8") as f:
87
- help_message = session.run("fotolab", "-h", silent=True)
81
+ help_message = session.run("uv", "run", "fotolab", "-h", silent=True)
88
82
  help_codeblock = f"\n\n```console\n{help_message}```\n\n"
89
83
 
90
84
  content = f.read()
@@ -105,7 +99,7 @@ def readme(session: nox.Session) -> None:
105
99
  "env",
106
100
  ]:
107
101
  help_message = session.run(
108
- "fotolab", subcommand, "-h", silent=True
102
+ "uv", "run", "fotolab", subcommand, "-h", silent=True
109
103
  )
110
104
  help_codeblock = f"\n\n```console\n{help_message}```\n\n"
111
105
 
@@ -127,72 +121,39 @@ def release(session: nox.Session) -> None:
127
121
  nox -s release -- minor
128
122
  nox -s release -- micro (default)
129
123
  """
130
- with open("fotolab/__init__.py", "r", encoding="utf8") as f:
131
- tree = ast.parse(f.read())
132
- current_version = None
133
- for node in ast.walk(tree):
134
- if isinstance(node, ast.Assign) and len(node.targets) == 1:
135
- target, value = node.targets[0], node.value
136
- if target.id == "__version__" and isinstance(
137
- value, ast.Constant
138
- ):
139
- current_version = value.s
140
- break
141
-
142
- if current_version is None:
143
- raise ValueError("Missing __version__ variable in __init__.py")
144
-
145
- before_version = Version(current_version)
146
-
147
- (major, minor, micro) = (
148
- before_version.major,
149
- before_version.minor,
150
- before_version.micro,
151
- )
152
- if "major" in session.posargs:
153
- major = major + 1
154
- minor = 0
155
- micro = 0
156
-
157
- if "minor" in session.posargs:
158
- minor = minor + 1
159
- micro = 0
160
-
161
- if "micro" in session.posargs or session.posargs == []:
162
- micro = micro + 1
163
-
164
- after_version = f"{major}.{minor}.{micro}"
165
-
166
- _search_and_replace(
167
- "fotolab/__init__.py", str(before_version), after_version
168
- )
169
-
170
- session.log(
171
- f"Bumping version from {before_version} to {after_version}"
172
- )
173
-
174
- date = datetime.date.today().strftime("%Y-%m-%d")
175
- before_header = "## [Unreleased]\n\n"
176
- after_header = f"## [Unreleased]\n\n## v{after_version} ({date})\n\n"
177
- _search_and_replace("CHANGELOG.md", before_header, after_header)
178
-
179
- session.run(
180
- "git",
181
- "commit",
182
- "--no-verify",
183
- "-am",
184
- f"Bump {after_version} release",
185
- external=True,
186
- )
187
-
188
- if input("Publish package to pypi? (y/n): ").lower() in ["y", "yes"]:
189
- session.run("flit", "build")
190
- session.run("flit", "publish")
191
-
192
-
193
- def _pipenv_install(session: nox.Session) -> None:
194
- session.install("pipenv")
195
- session.run("pipenv", "install", "--dev", env={"PIPENV_VERBOSITY": "-1"})
124
+ _uv_install(session)
125
+
126
+ parser = argparse.ArgumentParser(description="Release a semver version.")
127
+ parser.add_argument(
128
+ "semver",
129
+ type=str,
130
+ nargs="?",
131
+ help="The type of semver release to make.",
132
+ default="patch",
133
+ choices={"major", "minor", "patch"},
134
+ )
135
+ args = parser.parse_args(args=session.posargs)
136
+
137
+ session.run("uv", "version", "--bump", args.semver)
138
+ after_version = session.run("uv", "version", "--short", silent=True).strip()
139
+
140
+ date = datetime.date.today().strftime("%Y-%m-%d")
141
+ before_header = "## [Unreleased]\n\n"
142
+ after_header = f"## [Unreleased]\n\n## v{after_version} ({date})\n\n"
143
+ _search_and_replace("CHANGELOG.md", before_header, after_header)
144
+
145
+ session.run(
146
+ "git",
147
+ "commit",
148
+ "--no-verify",
149
+ "-am",
150
+ f"Bump {after_version} release",
151
+ external=True,
152
+ )
153
+
154
+ if input("Publish package to pypi? (y/n): ").lower() in ["y", "yes"]:
155
+ session.run("flit", "build")
156
+ session.run("flit", "publish")
196
157
 
197
158
 
198
159
  def _search_and_replace(file, search, replace) -> None:
@@ -202,3 +163,22 @@ def _search_and_replace(file, search, replace) -> None:
202
163
  f.seek(0)
203
164
  f.write(content)
204
165
  f.truncate()
166
+
167
+
168
+ def _uv_install(session: nox.Session) -> None:
169
+ """Install the project and its development dependencies using uv.
170
+
171
+ This also resolves the following error:
172
+ warning: `VIRTUAL_ENV=.nox/lint` does not match the project environment
173
+ path `.venv` and will be ignored
174
+
175
+ See https://nox.thea.codes/en/stable/cookbook.html#using-a-lockfile
176
+ """
177
+ session.run_install(
178
+ "uv",
179
+ "sync",
180
+ "--upgrade",
181
+ "--all-packages",
182
+ f"--python={session.virtualenv.location}",
183
+ env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
184
+ )