collect-framework-fox-task5 1.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,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(/tmp/test-install/bin/collect_framework:*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: collect-framework-fox-task5
3
+ Version: 1.0.0
4
+ Summary: Count unique characters in a string via CLI
5
+ Requires-Python: >=3.8
@@ -0,0 +1,54 @@
1
+ # collect_framework
2
+
3
+ A CLI tool that counts unique characters (characters appearing exactly once) in a string or text file.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install .
9
+ ```
10
+
11
+ For development (editable install):
12
+
13
+ ```bash
14
+ pip install -e .
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Count unique characters in a string
20
+
21
+ ```bash
22
+ python -m collect_framework --string "abbbccdf"
23
+ ```
24
+
25
+ Output: `3` (characters `a`, `d`, `f` appear once)
26
+
27
+ ### Count unique characters in a file
28
+
29
+ ```bash
30
+ python -m collect_framework --file path_to_text_file
31
+ ```
32
+
33
+ ### Both parameters
34
+
35
+ If both `--string` and `--file` are provided, `--file` takes priority and the string is ignored:
36
+
37
+ ```bash
38
+ python -m collect_framework --string "your string" --file path_to_text_file
39
+ ```
40
+
41
+ ### As an installed script
42
+
43
+ After installation, the command is also available directly:
44
+
45
+ ```bash
46
+ collect_framework --string "hello"
47
+ ```
48
+
49
+ ## Running Tests
50
+
51
+ ```bash
52
+ pip install pytest
53
+ python -m pytest tests/ -v
54
+ ```
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "collect-framework-fox-task5"
7
+ version = "1.0.0"
8
+ description = "Count unique characters in a string via CLI"
9
+ requires-python = ">=3.8"
10
+
11
+ [project.scripts]
12
+ collect_framework = "collect_framework.__main__:main"
13
+
14
+ [tool.hatch.build.targets.wheel]
15
+ packages = ["src/collect_framework"]
@@ -0,0 +1,10 @@
1
+ """Count unique characters in a string."""
2
+
3
+ from collections import Counter
4
+ from functools import lru_cache
5
+
6
+
7
+ @lru_cache
8
+ def count_elements(string: str) -> int:
9
+ """Return the number of characters that appear exactly once."""
10
+ return sum(1 for count in Counter(string).values() if count == 1)
@@ -0,0 +1,43 @@
1
+ """CLI entry point for collect_framework."""
2
+
3
+ import argparse
4
+
5
+ from collect_framework import count_elements
6
+
7
+
8
+ def create_parser() -> argparse.ArgumentParser:
9
+ parser = argparse.ArgumentParser(description="Count unique characters.")
10
+ parser.add_argument("--string", nargs="+", help="Input string")
11
+ parser.add_argument(
12
+ "--file",
13
+ nargs="+",
14
+ help="Path to text file(s) (higher priority than --string)",
15
+ )
16
+ return parser
17
+
18
+
19
+ def main(args=None):
20
+ parser = create_parser()
21
+ parsed = parser.parse_args(args)
22
+
23
+ if parsed.file:
24
+ parts = []
25
+ for filepath in parsed.file:
26
+ try:
27
+ with open(filepath, "r") as f:
28
+ parts.append(f.read())
29
+ except FileNotFoundError:
30
+ parser.error(f"File not found: {filepath}")
31
+ text = "".join(parts)
32
+ elif parsed.string is not None:
33
+ text = " ".join(parsed.string)
34
+ else:
35
+ parser.error("Provide --string or --file")
36
+
37
+ result = count_elements(text)
38
+ print(result)
39
+ return result
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -0,0 +1,117 @@
1
+ """Tests for collect_framework package."""
2
+
3
+ import pytest
4
+ from unittest.mock import patch, mock_open
5
+
6
+ from collect_framework import count_elements
7
+ from collect_framework.__main__ import main
8
+
9
+
10
+ @pytest.mark.parametrize("input_str, expected", [
11
+ pytest.param("abbbccdf", 3, id="example_from_spec (a, d, f)"),
12
+ pytest.param("abcdef", 6, id="all_unique"),
13
+ pytest.param("aabbcc", 0, id="no_unique"),
14
+ pytest.param("a", 1, id="single_char"),
15
+ pytest.param("", 0, id="empty_string"),
16
+ pytest.param("aaaa", 0, id="all_same_char"),
17
+ pytest.param("AaBb", 4, id="mixed_case (A and a are different)"),
18
+ pytest.param("a b b", 1, id="spaces (a unique, space and b repeated)"),
19
+ pytest.param("112233", 0, id="digits_no_unique"),
20
+ pytest.param("12345", 5, id="digits_all_unique"),
21
+ pytest.param("café", 4, id="unicode (c, a, f, é all unique)"),
22
+ pytest.param("x", 1, id="single_x"),
23
+ pytest.param("xx", 0, id="double_x"),
24
+ pytest.param("abcabc", 0, id="repeated_pattern"),
25
+ pytest.param("abcabcd", 1, id="one_unique_in_pattern"),
26
+ pytest.param("aAbBcC", 6, id="mixed_case_all_unique"),
27
+ pytest.param("!@#!@#$", 1, id="special_chars_one_unique"),
28
+ ])
29
+ def test_count_elements(input_str, expected):
30
+ assert count_elements(input_str) == expected
31
+
32
+
33
+ class TestCaching:
34
+
35
+ def setup_method(self):
36
+ count_elements.cache_clear()
37
+
38
+ def test_cache_returns_same_result(self):
39
+ first_call = count_elements("abbbccdf")
40
+ second_call = count_elements("abbbccdf")
41
+ assert first_call == second_call
42
+
43
+ def test_cache_stats_show_hit(self):
44
+ count_elements("hello")
45
+ count_elements("hello")
46
+ info = count_elements.cache_info()
47
+ assert info.hits == 1
48
+ assert info.misses == 1
49
+
50
+ def test_different_strings_cached_separately(self):
51
+ count_elements("abc")
52
+ count_elements("def")
53
+ info = count_elements.cache_info()
54
+ assert info.misses == 2
55
+ assert info.currsize == 2
56
+
57
+
58
+ class TestCLIArgs:
59
+ """Test argparse parameter handling with mocked arguments."""
60
+
61
+ def test_string_single_word(self, capsys):
62
+ result = main(["--string", "abbbccdf"])
63
+ assert result == 3
64
+ assert capsys.readouterr().out.strip() == "3"
65
+
66
+ def test_string_multiple_words(self, capsys):
67
+ result = main(["--string", "your", "string"])
68
+ # "your string" joined with space → count unique chars
69
+ assert result == count_elements("your string")
70
+
71
+ def test_file_flag(self, capsys):
72
+ m = mock_open(read_data="abcdef")
73
+ with patch("builtins.open", m):
74
+ result = main(["--file", "dummy.txt"])
75
+ m.assert_called_once_with("dummy.txt", "r")
76
+ assert result == 6
77
+ assert capsys.readouterr().out.strip() == "6"
78
+
79
+ def test_file_has_priority_over_string(self, capsys):
80
+ m = mock_open(read_data="xyz")
81
+ with patch("builtins.open", m):
82
+ result = main(["--string", "abbbccdf", "--file", "dummy.txt"])
83
+ m.assert_called_once_with("dummy.txt", "r")
84
+ assert result == 3 # x, y, z all unique
85
+ assert capsys.readouterr().out.strip() == "3"
86
+
87
+ def test_multiple_files(self, capsys):
88
+ m = mock_open(read_data="abc")
89
+ with patch("builtins.open", m):
90
+ result = main(["--file", "a.txt", "b.txt"])
91
+ assert m.call_count == 2
92
+
93
+ def test_no_args_raises_error(self):
94
+ with pytest.raises(SystemExit):
95
+ main([])
96
+
97
+
98
+ class TestFileReading:
99
+ """Test file reading with mocks."""
100
+
101
+ def test_reads_file_content(self, capsys):
102
+ m = mock_open(read_data="hello")
103
+ with patch("builtins.open", m):
104
+ result = main(["--file", "test.txt"])
105
+ m.assert_called_once_with("test.txt", "r")
106
+ assert result == 3 # h, e, o unique; l repeated
107
+
108
+ def test_missing_file_raises_error(self):
109
+ with patch("builtins.open", side_effect=FileNotFoundError):
110
+ with pytest.raises(SystemExit):
111
+ main(["--file", "nonexistent.txt"])
112
+
113
+ def test_empty_file(self, capsys):
114
+ m = mock_open(read_data="")
115
+ with patch("builtins.open", m):
116
+ result = main(["--file", "empty.txt"])
117
+ assert result == 0