aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--challenge-286/ppentchev/README217
-rw-r--r--challenge-286/ppentchev/blog.txt1
-rw-r--r--challenge-286/ppentchev/docs/index.md41
-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
-rw-r--r--challenge-286/ppentchev/tests/01-perl-ch-1.t2
-rw-r--r--challenge-286/ppentchev/tests/02-perl-ch-2.t4
-rw-r--r--challenge-286/ppentchev/tests/03-python-ch-1.t31
-rw-r--r--challenge-286/ppentchev/tests/04-python-ch-2.t47
-rw-r--r--challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm31
15 files changed, 569 insertions, 94 deletions
diff --git a/challenge-286/ppentchev/README b/challenge-286/ppentchev/README
index 1ceb3b34df..aadceb51f2 100644
--- a/challenge-286/ppentchev/README
+++ b/challenge-286/ppentchev/README
@@ -3,107 +3,170 @@ SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
SPDX-License-Identifier: BSD-2-Clause
-->
-# Parse a mini-language for selecting objects by tag or name
+# Peter Pentchev's solutions to PWC 286
-\[[Home][ringlet-parse-stages] | [GitLab][gitlab] | [PyPI][pypi] | [ReadTheDocs][readthedocs]\]
+\[[Home][ringlet-home] | [GitHub][github]
## Overview
-The `parse-stages` Python library may be used by other tools to group
-objects (e.g. [Tox] or [Nox] test environments) for step-by-step processing
-(e.g. running some tests in parallel, then only running others if the first
-group passes).
+This directory contains Peter Pentchev's solutions to the two tasks in
+the [Perl & Raku Weekly Challenge 286][pwc-286].
+
+## General remarks
+
+### Task 1: Self Spammer
+
+Most programming languages have (different or the same) string-processing
+functions that can:
+
+- read a file and split it into lines at the same time
+- split the whole contents read from a file into lines
+- split a string by some commonly-accepted set of whitespace characters
+
+The tricky part here is finding the program source.
+In most of the so-called "interpreted" languages, e.g. Perl, Python, Ruby et al,
+there is either a file-global variable or a function that one can call to
+obtain the path to the file containing this particular source code line
+(note that I put "interpreted" in quotes, since it is common for these languages to
+do a precompilation phase to some intermediate representation, e.g. byte-code).
+For compiled languages such as Rust, Go, C, C++, etc., this part may be a bit
+more difficult since the end result does not generally include the program source code,
+so this part may have to depend on some specifics of the build environment and
+only be capable to run directly from a build directory near to its source code.
+
+### Task 2: Order Game
+
+There are several approaches to solving this problem:
-The language used by the library is described in the
-[_Grouping stages_](language.md#grouping-stages-for-step-by-step-execution)
-section.
+- trivial: take a pair of numbers, flip a "do we want the smaller or the larger one" flag,
+ produce the corresponding result, move on to the next pair
+- keep this information in an object, feed it numbers one by one or in pairs, let it
+ update its inner state (or, if it is an immutable object, let it produce a new one with
+ the inner state updated)
+- keep this information in an object, feed it number by number or the whole sequence,
+ have it work as an iterator that produces numbers for the programming language's
+ constructs to consume
-[tox]: https://tox.wiki/en/latest/ "The tox automation project"
-[nox]: https://nox.thea.codes/en/stable/ "The nox flexible automation tool"
+Some languages have built-in constructs for dealing with iterators, others can
+handle immutable objects easily and efficiently, so the approach depends on
+the programming language.
-## Installation
+## Running the test suite
-A program that uses the `parse-stages` library should specify it in
-its list of requirements, e.g. using [PEP508][pep508] syntax:
+The `tests/` subdirectory contains a Perl test suite that outputs TAP, so that
+it can be run using `prove tests` (or, for the more adventurous, `prove -v tests`).
+
+The Python implementation has its own set of static checkers (linters).
+Those can be run from the `python/` directory using Tox 4.x - either directly
+(`tox run-parallel`) or using [the tox-stages tool][python-tox-stages]
+(`tox-stages run`).
- parse-stages >= 0.1.9, < 0.2
+### Task 1: Self Spammer
-[pep508]: https://peps.python.org/pep-0508/ "PEP 508 – Dependency specification for Python Software Packages"
+This task does not involve any input data (except for the program source itself),
+so the programs are executed without any parameters.
-## Parsing a stage specification
+The `PWCTest::Ch286` module in `tests/lib/` defines a `test_self_spammer` function that
+runs the specified program, checks whether it output a single line containing a single word, and
+then looks for that word in the specified source file (the program itself by default).
+Of course, for the Perl implementation there is a bit of a chicken-and-egg problem, since
+the test itself must implement the same algorithm to check whether the word that
+the program output is actually present as a separate word in the source file.
+I played around a bit with the idea of using an external `grep` program to look for
+the word, but it turns out that at least GNU grep has some... idionsyncrasies regarding
+character sequences like `\<`, `\>`, `'')`, etc, so the output of Perl's `quotemeta`
+function was not suitable for feeding to `grep` directly.
+Eh, let's hope my solution is correct anyway :)
-The `parse_spec()` function parses a string specification into
-a [BoolExpr][parse_stages.BoolExpr] object that may later be used to
-select matching objects (e.g. test environments).
+The `tests/03-python-ch-1.t` test first looks for a suitable Python 3 interpreter.
+If the `PYTHON3` environment variable is defined, it is used as the name or path of
+the Python 3 interpreter; oterhwise, `python3` is used.
+The test tries to run the Python 3 interpreter for a `pass` command; if that fails,
+the tests are skipped.
-``` py
- e_check = parse_stages.parse_spec("@check")
- e_check_quick = parse_stages.parse_spec("@check and @quick")
- e_check_no_ruff = parse_stages.parse_spec("@check and not ruff")
-
- specs = [(spec, parse_stages.parse_spec(spec)) for spec in args.stage_specs]
-```
+### Task 2: Order Game
-## Check whether an object matches a parsed specification
+This task takes some input, so there are two ways of running the program:
-The `parse-stages` library provides two base dataclasses for objects that
-may be matched against parsed expressions:
-[TaggedFrozen][parse_stages.TaggedFrozen] and [Tagged][parse_stages.Tagged].
-Both classes have the same members:
+- if the `PWC_FROM_STDIN` environment variable is not set to the exact value `1`,
+ the program runs the three sequences given as examples, and produces three numbers on
+ its standard output stream, each one on a line by itself.
+ In other words, the program must output `1\n0\n2\n` exactly.
+- if the `PWC_FROM_STDIN` environment variable is set, the program reads a single line of
+ text from its standard input, treats it as a sequence of decimal integers separated by
+ one or more whitespace characters, runs the order game on that sequence, and produces
+ a single number on a line by itself on its standard output stream.
-- [name][parse_stages.TaggedFrozen.name]: a string
-- [tags][parse_stages.TaggedFrozen.tags]: a list of strings
-- [get_keyword_haystacks()][parse_stages.TaggedFrozen.get_keyword_haystacks]:
- a method that returns a list of strings,
- `self.name` unless overridden
+The `PWCTest::Ch286` module in `tests/lib/` defines a `test_order_game_default` function that
+runs a program with `PWC_FROM_STDIN` unset and expects the exact output, and also
+a `test_order_game` function that runs a series of tests with different sequences,
+each time running the program with `PWC_FROM_STDIN` set to 1 and feeding it the sequence.
-When a `BoolExpr` object's [evaluate()][parse_stages.BoolExpr.evaluate]
-method is called for a specific
-`TaggedFrozen` or `Tagged` object, it checks whether the specification
-matches the tags and keywords defined for this object. Tags are matched
-exactly, while a keyword is considered to match if it is contained in
-the checked string; e.g. `pep` would match both `pep8` and `exp_pep563`,
-while `@black` would not match a `black-reformat` tag.
+If the implementation in any language should provide more than one method, then
+the program should honor the `PWC_METHOD` environment variable.
+The value "0" indicates the use of the most natural method for the language,
+the value "1" indicates the use of an alternative method, and if there are more than two,
+then the values "2", "3", etc, are used to select them.
+If `PWC_METHOD` is set to a non-numeric value or to a value that is higher than
+the index of the last supported methods, it is ignored and the program proceeds as if
+`PWC_METHOD` was set to "0".
-The `get_keyword_haystacks()` method returns the strings to look in for
-matching keywords. By default, it only returns the `name` field;
-however, it may be extended, e.g. for Nox sessions it may also return
-the name of the Python function that implements the session, for test
-classes with methods it may return the class name and the method name, etc.
+The `tests/02-perl-ch-2.t` test runs these functions on the Perl implementation and
+produces TAP output suitable for the `prove` tool.
+
+The `tests/03-python-ch-2.t` test first looks for a suitable Python 3 interpreter.
+If the `PYTHON3` environment variable is defined, it is used as the name or path of
+the Python 3 interpreter; oterhwise, `python3` is used.
+The test tries to run the Python 3 interpreter for a `pass` command; if that fails,
+the tests are skipped.
+
+## Implementation details
+
+### Task 1: Self Spammer
+
+#### Perl
+
+We use the [FindBin core module][perl-findbin] and its `$Bin` and `$Script` variables to
+figure out where the program source is.
+
+#### Python
-``` py
- # Obtain a list (okay, a dictionary) of test environments in some way
- tox_envs = get_tox_environments() # {"ruff": {"tags": ["check", "quick"]}, ...}
+We use Python's built-in `__file__` pseudoglobal variable to find the path to our source file.
+Then we use `random.choice()` to select a random word directly without bothering with
+an index into the words array.
- # Convert them to objects that the parsed expressions can match
- all_envs = [
- parse_stages.TaggedFrozen(name, env["tags"])
- for name, env in tox_envs.items()
- ]
+### Task 2: Order Game
+
+#### Perl
+
+The Perl solution has three major functions:
+
+- `round_trivial` - run a single round on a list, producing a list with half the number of
+ elements using the trivial approach: take numbers in pairs, keep a flag, produce
+ the smaller or the larger one as the flag says, flip it.
+- `run_order_game` - run the whole order game on the specified sequence of numbers.
+ This function currently only uses a single "run a round" implementation, `round_trivial`.
+ The `PWC_METHOD` environment variable is ignored even if set.
+- `parse_stdin` - read a line of numbers from the standard input, break it down into
+ a list of integers.
- # Or define our own class that may hold additional information
- @dataclasses.dataclass(frozen=True)
- class TestEnv(parse_stages.TaggedFrozen):
- """A single test environment: name, tags, etc."""
- ...
+#### Python
- all_envs = [TestEnv(name, env["tags"], ...) for name, env in tox_envs.items()]
-
- # Select the ones that match the "@check" expression
- matched = [env for env in all_envs if e_check.evaluate(env)]
-
- # Or if we only care about the names...
- quick_names = [env.name for env in all_envs if e_check_quick.evaluate(env)]
-```
+The Python solution defines an `OrderIter` class and an `OrderIterState` enumeration.
+The class serves as an iterator, stashing a value and then performing a `min()` or
+`max()` operation on the next value fetched from the supplied input.
+The `OrderState` enumeration is used to keep track of the stash/yield state.
## Contact
-The `parse-stages` library was written by [Peter Pentchev][roam].
-It is developed in [a GitLab repository][gitlab]. This documentation is
-hosted at [Ringlet][ringlet-parse-stages] with a copy at [ReadTheDocs][readthedocs].
+These solutions were written by [Peter Pentchev][roam].
+They are developed in [the common PWC-club GitHub repository][github].
+This documentation is hosted at [Ringlet][ringlet-home].
[roam]: mailto:roam@ringlet.net "Peter Pentchev"
-[gitlab]: https://gitlab.com/ppentchev/parse-stages "The parse-stages GitLab repository"
-[pypi]: https://pypi.org/project/parse-stages/ "The parse-stages Python Package Index page"
-[readthedocs]: https://parse-stages.readthedocs.io/ "The parse-stages ReadTheDocs page"
-[ringlet-parse-stages]: https://devel.ringlet.net/devel/parse-stages/ "The Ringlet parse-stages homepage"
+[github]: https://github.com/manwar/perlweeklychallenge-club/tree/master/challenge-286/ppentchev "These solutions at GitHub"
+[ringlet-home]: https://devel.ringlet.net/misc/perlweeklychallenge-club/286/ "This documentation at Ringlet"
+
+[perl-findbin]: https://perldoc.perl.org/FindBin "The FindBin Perl core module"
+[pwc-286]: https://theweeklychallenge.org/blog/perl-weekly-challenge-286/ "The 286th Perl & Raku Weekly Challenge"
+[python-tox-stages]: https://devel.ringlet.net/devel/test-stages "Run Tox tests in groups"
diff --git a/challenge-286/ppentchev/blog.txt b/challenge-286/ppentchev/blog.txt
new file mode 100644
index 0000000000..275aac25cb
--- /dev/null
+++ b/challenge-286/ppentchev/blog.txt
@@ -0,0 +1 @@
+https://devel.ringlet.net/misc/perlweeklychallenge-club/286/
diff --git a/challenge-286/ppentchev/docs/index.md b/challenge-286/ppentchev/docs/index.md
index 1a1622a804..388239e6f1 100644
--- a/challenge-286/ppentchev/docs/index.md
+++ b/challenge-286/ppentchev/docs/index.md
@@ -5,7 +5,7 @@ SPDX-License-Identifier: BSD-2-Clause
# Peter Pentchev's solutions to PWC 286
-\[[Home][ringlet-home] | [GitHub][github]
+\[[Home][ringlet-home] | [GitHub][github]\]
## Overview
@@ -56,6 +56,11 @@ the programming language.
The `tests/` subdirectory contains a Perl test suite that outputs TAP, so that
it can be run using `prove tests` (or, for the more adventurous, `prove -v tests`).
+The Python implementation has its own set of static checkers (linters).
+Those can be run from the `python/` directory using Tox 4.x - either directly
+(`tox run-parallel`) or using [the tox-stages tool][python-tox-stages]
+(`tox-stages run`).
+
### Task 1: Self Spammer
This task does not involve any input data (except for the program source itself),
@@ -73,6 +78,12 @@ character sequences like `\<`, `\>`, `'')`, etc, so the output of Perl's `quotem
function was not suitable for feeding to `grep` directly.
Eh, let's hope my solution is correct anyway :)
+The `tests/03-python-ch-1.t` test first looks for a suitable Python 3 interpreter.
+If the `PYTHON3` environment variable is defined, it is used as the name or path of
+the Python 3 interpreter; oterhwise, `python3` is used.
+The test tries to run the Python 3 interpreter for a `pass` command; if that fails,
+the tests are skipped.
+
### Task 2: Order Game
This task takes some input, so there are two ways of running the program:
@@ -103,16 +114,30 @@ the index of the last supported methods, it is ignored and the program proceeds
The `tests/02-perl-ch-2.t` test runs these functions on the Perl implementation and
produces TAP output suitable for the `prove` tool.
+The `tests/03-python-ch-2.t` test first looks for a suitable Python 3 interpreter.
+If the `PYTHON3` environment variable is defined, it is used as the name or path of
+the Python 3 interpreter; oterhwise, `python3` is used.
+The test tries to run the Python 3 interpreter for a `pass` command; if that fails,
+the tests are skipped.
+
## Implementation details
-## Task 1: Self Spammer
+### Task 1: Self Spammer
-### Perl
+#### Perl
We use the [FindBin core module][perl-findbin] and its `$Bin` and `$Script` variables to
figure out where the program source is.
-## Task 2: Order Game
+#### Python
+
+We use Python's built-in `__file__` pseudoglobal variable to find the path to our source file.
+Then we use `random.choice()` to select a random word directly without bothering with
+an index into the words array.
+
+### Task 2: Order Game
+
+#### Perl
The Perl solution has three major functions:
@@ -125,6 +150,13 @@ The Perl solution has three major functions:
- `parse_stdin` - read a line of numbers from the standard input, break it down into
a list of integers.
+#### Python
+
+The Python solution defines an `OrderIter` class and an `OrderIterState` enumeration.
+The class serves as an iterator, stashing a value and then performing a `min()` or
+`max()` operation on the next value fetched from the supplied input.
+The `OrderState` enumeration is used to keep track of the stash/yield state.
+
## Contact
These solutions were written by [Peter Pentchev][roam].
@@ -137,3 +169,4 @@ This documentation is hosted at [Ringlet][ringlet-home].
[perl-findbin]: https://perldoc.perl.org/FindBin "The FindBin Perl core module"
[pwc-286]: https://theweeklychallenge.org/blog/perl-weekly-challenge-286/ "The 286th Perl & Raku Weekly Challenge"
+[python-tox-stages]: https://devel.ringlet.net/devel/test-stages "Run Tox tests in groups"
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'
diff --git a/challenge-286/ppentchev/tests/01-perl-ch-1.t b/challenge-286/ppentchev/tests/01-perl-ch-1.t
index deec7c59f4..0caef63ae1 100644
--- a/challenge-286/ppentchev/tests/01-perl-ch-1.t
+++ b/challenge-286/ppentchev/tests/01-perl-ch-1.t
@@ -17,5 +17,5 @@ use constant PROG => 'perl/scripts/ch-1.pl';
plan tests => 1;
subtest self_spammer => sub {
- test_self_spammer PROG;
+ test_self_spammer [PROG];
};
diff --git a/challenge-286/ppentchev/tests/02-perl-ch-2.t b/challenge-286/ppentchev/tests/02-perl-ch-2.t
index f68867b7d2..c808028888 100644
--- a/challenge-286/ppentchev/tests/02-perl-ch-2.t
+++ b/challenge-286/ppentchev/tests/02-perl-ch-2.t
@@ -17,14 +17,14 @@ use constant PROG => 'perl/scripts/ch-2.pl';
plan tests => 2;
subtest order_game_default => sub {
- test_order_game_default PROG;
+ test_order_game_default [PROG];
};
subtest order_game => sub {
plan tests => test_order_game_count;
for my $idx (1..test_order_game_count) {
subtest "run $idx" => sub {
- test_order_game PROG, $idx - 1;
+ test_order_game [PROG], $idx - 1;
};
}
};
diff --git a/challenge-286/ppentchev/tests/03-python-ch-1.t b/challenge-286/ppentchev/tests/03-python-ch-1.t
new file mode 100644
index 0000000000..0b3066753f
--- /dev/null
+++ b/challenge-286/ppentchev/tests/03-python-ch-1.t
@@ -0,0 +1,31 @@
+#!/usr/bin/perl
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+use v5.16;
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib 'tests/lib';
+
+use PWCTest::Ch286 qw(find_python3 test_self_spammer);
+
+my $py3;
+BEGIN {
+ $py3 = find_python3;
+}
+
+if (!defined $py3) {
+ plan skip_all => 'no Python 3 interpreter found';
+ exit 0;
+}
+
+plan tests => 1;
+
+use constant PROG => 'python/scripts/ch-1.py';
+
+subtest self_spammer => sub {
+ test_self_spammer [$py3, '-B', '-u', '--', PROG], PROG;
+};
diff --git a/challenge-286/ppentchev/tests/04-python-ch-2.t b/challenge-286/ppentchev/tests/04-python-ch-2.t
new file mode 100644
index 0000000000..1d36b91a85
--- /dev/null
+++ b/challenge-286/ppentchev/tests/04-python-ch-2.t
@@ -0,0 +1,47 @@
+#!/usr/bin/perl
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+use v5.16;
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib 'tests/lib';
+
+use PWCTest::Ch286 qw(
+ find_python3
+ test_order_game
+ test_order_game_count
+ test_order_game_default
+);
+
+my $py3;
+BEGIN {
+ $py3 = find_python3;
+}
+
+if (!defined $py3) {
+ plan skip_all => 'no Python 3 interpreter found';
+ exit 0;
+}
+
+plan tests => 2;
+
+use constant PROG => 'python/scripts/ch-2.py';
+
+my $py3_prog = [$py3, '-B', '-u', '--', PROG];
+
+subtest order_game_default => sub {
+ test_order_game_default $py3_prog;
+};
+
+subtest order_game => sub {
+ plan tests => test_order_game_count;
+ for my $idx (1..test_order_game_count) {
+ subtest "run $idx" => sub {
+ test_order_game $py3_prog, $idx - 1;
+ };
+ }
+};
diff --git a/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm b/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm
index 31bbdc6000..e03d43e6e9 100644
--- a/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm
+++ b/challenge-286/ppentchev/tests/lib/PWCTest/Ch286.pm
@@ -19,6 +19,7 @@ use Test::Command qw();
use Test::More;
our @EXPORT_OK = qw(
+ find_python3
test_order_game
test_order_game_count
test_order_game_default
@@ -45,8 +46,9 @@ my @TEST_ORDER_SEQUENCES = (
my $re_single_line = qr{^ (?<word> [^\n\s]+ ) \n $}x;
sub test_self_spammer($;$) {
- my ($prog, $source) = @_;
- $source //= $prog;
+ my ($cmd, $source) = @_;
+ $source //= $cmd->[0];
+ my $prog = "\`@{$cmd}\`";
# So... to test this, we basically have to implement it, right?
my @words = do {
@@ -63,7 +65,7 @@ sub test_self_spammer($;$) {
for my $run (1..NUM_TESTS) {
diag "About to run $prog";
- my $cmd = Test::Command->new(cmd => [$prog]);
+ my $cmd = Test::Command->new(cmd => $cmd);
$cmd->exit_is_num(0, "$prog exited with code 0");
$cmd->stdout_isnt_eq('', "$prog output something");
@@ -72,7 +74,7 @@ sub test_self_spammer($;$) {
my $output = $cmd->stdout_value;
if ($output !~ $re_single_line) {
fail "$prog output a single line containing a single word";
- skip 1, "no program output to look for in $source";
+ skip "no program output to look for in $source", 1;
return;
}
pass "$prog output a single line containing a single word";
@@ -85,13 +87,14 @@ sub test_self_spammer($;$) {
}
sub test_order_game_default($) {
- my ($prog) = @_;
+ my ($cmd) = @_;
+ my $prog = "\`@{$cmd}\`";
plan tests => 2;
- my $cmd = Test::Command->new(cmd => ['env', 'PWC_FROM_STDIN=', $prog]);
- $cmd->exit_is_num(0, "$prog exited with code 0");
- $cmd->stdout_is_eq("1\n0\n2\n", "$prog produced the correct output in autotest mode");
+ my $auto_cmd = Test::Command->new(cmd => ['env', 'PWC_FROM_STDIN=', @{$cmd}]);
+ $auto_cmd->exit_is_num(0, "$prog exited with code 0");
+ $auto_cmd->stdout_is_eq("1\n0\n2\n", "$prog produced the correct output in autotest mode");
}
sub test_order_game_count() {
@@ -99,7 +102,8 @@ sub test_order_game_count() {
}
sub test_order_game($ $) {
- my ($prog, $idx) = @_;
+ my ($cmd, $idx) = @_;
+ my $prog = "\`@{$cmd}\`";
plan tests => 2;
@@ -121,7 +125,7 @@ sub test_order_game($ $) {
dup2(fileno $child_out, 1) or die "Child: could not dup2 child_out onto stdout: $!\n";
$ENV{PWC_FROM_STDIN} = '1';
- exec { $prog } $prog;
+ exec { $cmd->[0] } @{$cmd};
die "Child: could not execute $prog: $!\n";
}
@@ -143,4 +147,11 @@ sub test_order_game($ $) {
is $?, 0, "$prog exited with code 0";
}
+sub find_python3()
+{
+ my $prog = $ENV{PYTHON3} || 'python3';
+ my $res = system { $prog } ($prog, '-c', 'pass');
+ (defined $res && $res == 0) ? $prog : undef
+}
+
1;