aboutsummaryrefslogtreecommitdiff
path: root/challenge-286/ppentchev/python
diff options
context:
space:
mode:
authorPeter Pentchev <roam@ringlet.net>2024-09-10 23:49:05 +0300
committerPeter Pentchev <roam@ringlet.net>2024-09-11 21:32:24 +0300
commit7b0cce752eca91696664c2bb0de3c5e0395f3fda (patch)
tree9f9c62c53aecdc669cec6ff05d19a478b0895bf7 /challenge-286/ppentchev/python
parentcbf28e6e924b7cd24d1614a5ad28d801f0e85dea (diff)
downloadperlweeklychallenge-club-7b0cce752eca91696664c2bb0de3c5e0395f3fda.tar.gz
perlweeklychallenge-club-7b0cce752eca91696664c2bb0de3c5e0395f3fda.tar.bz2
perlweeklychallenge-club-7b0cce752eca91696664c2bb0de3c5e0395f3fda.zip
Add Peter Pentchev's Python solutions to 286
Also fix a couple of things in the other files: - make the TAP test functions accept an arrayref for the command to run instead of a single string, since we want to run the Python solution using `python3 -B -u /path/to/the/program` - fix the use of Test::More::skip() in the Self Spammer test function - fix a shadowed variable in the Order Game test function - correct the indentation in the documentation's "Implementation details" section - make the README file a copy of docs/index.md instead of a wrong copy of a completely different project's README file - point blog.txt to the Ringlet copy of this challenge's documentation
Diffstat (limited to 'challenge-286/ppentchev/python')
-rw-r--r--challenge-286/ppentchev/python/README.md8
-rw-r--r--challenge-286/ppentchev/python/pyproject.toml19
-rw-r--r--challenge-286/ppentchev/python/requirements/ruff.txt4
-rw-r--r--challenge-286/ppentchev/python/ruff-base.toml39
-rwxr-xr-xchallenge-286/ppentchev/python/scripts/ch-1.py39
-rwxr-xr-xchallenge-286/ppentchev/python/scripts/ch-2.py109
-rw-r--r--challenge-286/ppentchev/python/tox.ini71
7 files changed, 289 insertions, 0 deletions
diff --git a/challenge-286/ppentchev/python/README.md b/challenge-286/ppentchev/python/README.md
new file mode 100644
index 0000000000..f7121289e5
--- /dev/null
+++ b/challenge-286/ppentchev/python/README.md
@@ -0,0 +1,8 @@
+<!--
+SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+SPDX-License-Identifier: BSD-2-Clause
+-->
+
+# pwc-286 - Python solutions to the Perl Weekly Challenge 286 tasks
+
+See the `ppentchev/docs/index.md` file for details.
diff --git a/challenge-286/ppentchev/python/pyproject.toml b/challenge-286/ppentchev/python/pyproject.toml
new file mode 100644
index 0000000000..1b9e32ae87
--- /dev/null
+++ b/challenge-286/ppentchev/python/pyproject.toml
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+[tool.mypy]
+strict = true
+
+[tool.ruff]
+extend = "ruff-base.toml"
+output-format = "concise"
+preview = true
+
+[tool.ruff.lint]
+select = ["ALL"]
+
+[tool.test-stages]
+stages = [
+ "@check and @quick and not @manual",
+ "@check and not @manual",
+]
diff --git a/challenge-286/ppentchev/python/requirements/ruff.txt b/challenge-286/ppentchev/python/requirements/ruff.txt
new file mode 100644
index 0000000000..1c69e31418
--- /dev/null
+++ b/challenge-286/ppentchev/python/requirements/ruff.txt
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+ruff == 0.6.4
diff --git a/challenge-286/ppentchev/python/ruff-base.toml b/challenge-286/ppentchev/python/ruff-base.toml
new file mode 100644
index 0000000000..03cd7fa385
--- /dev/null
+++ b/challenge-286/ppentchev/python/ruff-base.toml
@@ -0,0 +1,39 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+target-version = "py311"
+line-length = 100
+
+[lint]
+select = []
+ignore = [
+ # No blank lines before the class docstring, TYVM
+ "D203",
+
+ # The multi-line docstring summary starts on the same line
+ "D213",
+
+ # We do not document everything in the docstring
+ "DOC201",
+ "DOC402",
+ "DOC501",
+
+ # The /x regex modifier is common enough in many languages
+ "FURB167",
+]
+
+[lint.flake8-copyright]
+notice-rgx = "(?x) SPDX-FileCopyrightText: \\s \\S"
+
+[lint.isort]
+force-single-line = true
+known-first-party = ["pwc_286"]
+lines-after-imports = 2
+single-line-exclusions = ["collections.abc", "typing"]
+
+[lint.per-file-ignores]
+# This is a command-line tool; console output is part of its job.
+"src/pwc_286/__main__.py" = ["T201"]
+
+# This is a test suite
+"tests/unit/**.py" = ["S101", "T201"]
diff --git a/challenge-286/ppentchev/python/scripts/ch-1.py b/challenge-286/ppentchev/python/scripts/ch-1.py
new file mode 100755
index 0000000000..a3470d1bd4
--- /dev/null
+++ b/challenge-286/ppentchev/python/scripts/ch-1.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python3
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+"""Pick a random word from this script's source file and output it."""
+
+from __future__ import annotations
+
+import pathlib
+import random
+import typing
+
+
+if typing.TYPE_CHECKING:
+ from typing import Final
+
+
+class EmptyWordError(RuntimeError):
+ """An empty word was chosen, `str.split()` is not suppoed to do that."""
+
+ words: list[str]
+ """The words among which the empty word was found."""
+
+ def __str__(self) -> str:
+ """Provide a human-readable error message."""
+ return f"Internal error: empty word in {self.words!r}"
+
+
+def main() -> None:
+ """Find the source file, select a word, output it."""
+ words: Final = pathlib.Path(__file__).read_text(encoding="UTF-8").split()
+ chosen_one: Final = random.choice(words) # noqa: S311 # no cryptography here
+ if not chosen_one:
+ raise EmptyWordError(words)
+
+ print(chosen_one) # noqa: T201 # this is the whole point of this program
+
+
+if __name__ == "__main__":
+ main()
diff --git a/challenge-286/ppentchev/python/scripts/ch-2.py b/challenge-286/ppentchev/python/scripts/ch-2.py
new file mode 100755
index 0000000000..b8c0819589
--- /dev/null
+++ b/challenge-286/ppentchev/python/scripts/ch-2.py
@@ -0,0 +1,109 @@
+#!/usr/bin/python3
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+"""Pick a random word from this script's source file and output it."""
+
+from __future__ import annotations
+
+import enum
+import os
+import sys
+import typing
+
+
+if typing.TYPE_CHECKING:
+ from collections.abc import Callable, Iterable, Iterator
+ from typing import Final, Self
+
+
+TEST_SEQUENCES: Final = [
+ [2, 1, 4, 5, 6, 3, 0, 2],
+ [0, 5, 3, 2],
+ [9, 2, 1, 4, 5, 6, 0, 7, 3, 1, 3, 5, 7, 9, 0, 8],
+]
+"""The sample sequences from the problem."""
+
+
+class OrderIterState(enum.IntEnum):
+ """The states that an `OrderIter` object may be in."""
+
+ STASH = 0
+ """Stash a value to pass to `min()` or `max()` later."""
+
+ YIELD = 1
+ """Perform a `min()` or `max()` operation, yield the result."""
+
+
+class OrderIter:
+ """Iterate over the numbers of a single round."""
+
+ ints: Iterator[int]
+ """The list of integers to process."""
+
+ state: OrderIterState
+ """The current state of the iterator."""
+
+ stashed: int
+ """The last stashed number."""
+
+ op: Callable[[int, int], int]
+ """The operation to perform when yielding a value."""
+
+ def __init__(self, ints: Iterable[int]) -> None:
+ """Construct an `OrderIter` object with the specified numbers to process."""
+ self.ints = iter(ints)
+ self.state = OrderIterState.STASH
+ self.stashed = -616
+ self.op = min
+
+ def __iter__(self) -> Self:
+ """Return the `OrderIter` object itself as an iterator."""
+ return self
+
+ def __next__(self) -> int:
+ """Calculate and return the next number."""
+ match self.state:
+ case OrderIterState.STASH:
+ self.stashed = next(self.ints)
+ self.state = OrderIterState.YIELD
+ return next(self)
+
+ case OrderIterState.YIELD:
+ res: Final = self.op(self.stashed, next(self.ints))
+ self.state = OrderIterState.STASH
+ self.op = max if self.op == min else min
+ return res
+
+
+def order_iter(ints: list[int]) -> list[int]:
+ """Run a single round using an `OrderIter` object."""
+ return list(OrderIter(ints))
+
+
+def run_order_game(ints: list[int]) -> int:
+ """Run the order game until there is only a single integer left."""
+ if not ints:
+ raise RuntimeError(repr(ints))
+
+ while len(ints) > 1:
+ ints = order_iter(ints)
+
+ return ints[0]
+
+
+def parse_stdin() -> list[int]:
+ """Read a line from the standard input, parse it as a list of integers."""
+ return [int(word) for word in sys.stdin.readline().split() if word]
+
+
+def main() -> None:
+ """Find the source file, select a word, output it."""
+ if os.environ.get("PWC_FROM_STDIN", "") == "1":
+ print(run_order_game(parse_stdin())) # noqa: T201 # this is the whole point
+ else:
+ for ints in TEST_SEQUENCES:
+ print(run_order_game(ints)) # noqa: T201 # this is the whole point
+
+
+if __name__ == "__main__":
+ main()
diff --git a/challenge-286/ppentchev/python/tox.ini b/challenge-286/ppentchev/python/tox.ini
new file mode 100644
index 0000000000..5d113b5fd0
--- /dev/null
+++ b/challenge-286/ppentchev/python/tox.ini
@@ -0,0 +1,71 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+[tox]
+minversion = 4.1
+envlist =
+ ruff
+ format
+ mypy
+isolated_build = True
+
+[defs]
+pyfiles =
+ scripts/ch-1.py \
+ scripts/ch-2.py
+
+[testenv:ruff]
+skip_install = True
+tags =
+ check
+ ruff
+ quick
+deps =
+ -r requirements/ruff.txt
+commands =
+ ruff check -- {[defs]pyfiles}
+
+[testenv:format]
+skip_install = True
+tags =
+ check
+ quick
+deps =
+ -r requirements/ruff.txt
+commands =
+ ruff check --config ruff-base.toml --select=D,I --diff -- {[defs]pyfiles}
+ ruff format --config ruff-base.toml --check --diff -- {[defs]pyfiles}
+
+[testenv:reformat]
+skip_install = True
+tags =
+ format
+ manual
+deps =
+ -r requirements/ruff.txt
+commands =
+ ruff check --config ruff-base.toml --select=D,I --fix -- {[defs]pyfiles}
+ ruff format --config ruff-base.toml -- {[defs]pyfiles}
+
+[testenv:mypy]
+skip_install = True
+tags =
+ check
+deps =
+ mypy >= 1, < 2
+setenv =
+ MYPYPATH = {toxinidir}/stubs
+commands =
+ mypy {[defs]pyfiles}
+
+[testenv:pyupgrade]
+skip_install = True
+tags =
+ check
+ manual
+deps =
+ pyupgrade >= 3, < 4
+allowlist_externals =
+ sh
+commands =
+ sh -c 'pyupgrade --py311-plus scripts/*.py'