Skip to content

Commit bd7c82f

Browse files
committed
Merge development/remastering into master
2 parents 17c81fd + 6c895a0 commit bd7c82f

File tree

15 files changed

+732
-149
lines changed

15 files changed

+732
-149
lines changed

changelog.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
- [ ] feature: implement r/w stream edit using reading by chunks instead of by lines
11+
- [ ] enhancement: change dictionary format: better compress
12+
- [ ] feature: extend dictionary: add more words, handle separately lower/upper-cased specific words
13+
- [ ] feature: add lint option
14+
- [ ] feature: add replacement stats
15+
16+
## [0.1.0] — 2025-02-16
17+
18+
`md.language.yofication` package initial version
19+
20+
# Archived versions
21+
22+
<details><summary>archived "yoficator" package versions</summary>
23+
824
## 0.1.7 — 2025-02-15
925
### Changed
1026

@@ -52,3 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5268
## Unversioned — 2015-11-17
5369

5470
Initial release
71+
72+
</details>
73+
74+
[0.1.0]: https://github.com/md-py/md.language.yofication/releases/tag/0.1.0
46.8 KB
Loading
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@startuml
2+
3+
skinparam class {
4+
BackgroundColor #ebebeb
5+
ArrowColor #333
6+
BorderColor #333
7+
}
8+
9+
skinparam lineType ortho
10+
11+
package md.language.yoficate {
12+
interface LoadDictionaryInterface {
13+
+ load(file_path: str) -> DictionaryType
14+
+ supports(file_path: str) -> bool
15+
}
16+
17+
interface DictionaryInterface {
18+
+ find(word: str) -> typing.Optional[str]
19+
+ has(word: str) -> bool
20+
}
21+
22+
interface YoficateInterface {
23+
+ text(text: str) -> str
24+
+ word(word: str) -> str
25+
}
26+
27+
class BytesStreamLoadDictionary {
28+
+ load(stream: typing.IO[bytes], encoding: str = 'utf-8') -> DictionaryType
29+
}
30+
31+
class TextFileLoadDictionary implements LoadDictionaryInterface {
32+
- bytes_stream_load_dictionary: BytesStreamLoadDictionary
33+
+ load(file_path: str) -> DictionaryType
34+
+ supports(file_path: str) -> bool
35+
}
36+
37+
class Bz2TextFileLoadDictionary implements LoadDictionaryInterface {
38+
- bytes_stream_load_dictionary: BytesStreamLoadDictionary
39+
+ load(file_path: str) -> DictionaryType
40+
+ supports(file_path: str) -> bool
41+
}
42+
43+
class MappingDictionary implements DictionaryInterface {
44+
- dictionary: DictionaryType
45+
+ find(word: str) -> typing.Optional[str]
46+
+ has(word: str) -> bool
47+
}
48+
49+
class RegularExpressionYoficate implements YoficateInterface {
50+
- dictionary: DictionaryInterface
51+
- e_word_regexp: re.Pattern
52+
+ text(text: str) -> str
53+
+ word(word: str) -> str
54+
}
55+
56+
TextFileLoadDictionary *-down-> BytesStreamLoadDictionary
57+
Bz2TextFileLoadDictionary *-down-> BytesStreamLoadDictionary
58+
RegularExpressionYoficate *-> DictionaryInterface
59+
}
60+
61+
@enduml

docs/_static/architecture-overview.class-diagram.svg

Lines changed: 87 additions & 0 deletions
Loading

docs/index.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# md.language.yofication
2+
3+
md.language.yofication component provides a cyrillic text yofication (ёфикация) API and CLI application.
4+
5+
This is remastered version of [Yoficator](https://github.com/unabashed/yoficator)
6+
originally developed by unabashed.
7+
8+
## Architecure overview
9+
10+
[![architecture overview class diagram](_static/architecture-overview.class-diagram.svg)](_static/architecture-overview.class-diagram.svg)
11+
12+
## Component overview
13+
14+
```python3
15+
# Type
16+
DictionaryType = typing.Mapping[str, str]
17+
18+
# ... alias to primary implementation
19+
DefaultYoficate = RegularExpressionYoficate
20+
21+
# ... function to load built-in dictionary
22+
def get_builtin_dictionary(locale: typing.Literal['ru_RU'] = 'ru_RU') -> MappingDictionary: ...
23+
```
24+
25+
## Installation
26+
27+
```sh
28+
pip install md.language.yofication --index https://source.md.land/python/
29+
```
30+
31+
## Usage
32+
33+
CLI Application provides next options:
34+
35+
- `--no-replace` (DEFAULT) — disables original files modification. Modified content is being printed
36+
to standard output (STDOUT). Conflicts with `--replace` option. Makes no sense when few files arguments are specified.
37+
- `--replace` — enables original files modifications. Conflicts with `--no-replace` option.
38+
Makes no sense when no files arguments were specified.
39+
40+
For more details see program help:
41+
42+
```sh
43+
python3 -m md.language.yofication -h
44+
```
45+
46+
Operations with files:
47+
48+
1. Prints the modified text content to standard output (STDOUT) **without changing the file**
49+
(the `--no-replace` option is the default).
50+
```sh
51+
python3 -m md.language.yofication ./file.txt # prints to STDOUT
52+
```
53+
2. Replaces specified file with yoficated content
54+
```sh
55+
python3 -m md.language.yofication --replace ./file.txt # replace in-place
56+
python3 -m md.language.yofication --replace ./file.txt ./file2.txt ./file3.txt # replaces files
57+
find . -type f -iname '*.txt' -exec python3 -m md.language.yofication --replace {} \+ # replaces files
58+
```
59+
60+
Operation standard input (STDIN):
61+
62+
1. Reads text from standard input (STDIN) and writes modified text to standard output (STDOUT).
63+
```sh
64+
cat ./file.txt | python3 -m md.language.yofication # reads from STDIN, prints to STDOUT
65+
echo "Где ее книга?" | python3 -m md.language.yofication
66+
python3 -m md.language.yofication <<< "Где ее книга?"
67+
```
68+
2. Interactive mode:
69+
```
70+
$ python3 -m md.language.yofication
71+
Где ее книга?
72+
Где её книга?
73+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from ._yofication import *
2+
from . import *
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import os.path
2+
import sys
3+
import argparse
4+
import typing
5+
import concurrent.futures
6+
7+
8+
import md.language.yofication
9+
10+
__all__ = (
11+
'ApplicationEchoLines',
12+
'ApplicationReplaceLines',
13+
'create_cli_parser',
14+
'main',
15+
)
16+
17+
18+
class ApplicationEchoLines:
19+
def __init__(self, yoficate: md.language.yofication.YoficateInterface) -> None:
20+
self._yoficate = yoficate
21+
22+
def run(self, input_stream: typing.IO[str]) -> None:
23+
try:
24+
for line in iter(input_stream):
25+
print(self._yoficate.text(text=line.rstrip('\n')))
26+
except (KeyboardInterrupt, EOFError):
27+
pass
28+
29+
30+
class ApplicationReplaceLines:
31+
def __init__(self, yoficate: md.language.yofication.YoficateInterface) -> None:
32+
self._yoficate = yoficate
33+
34+
def run(self, file_path_list: typing.List[str]) -> None:
35+
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
36+
for file_path in file_path_list:
37+
executor.submit(self._replace_file, file_path=file_path)
38+
39+
def _replace_file(self, file_path: str) -> None:
40+
directory, filename = os.path.split(file_path)
41+
42+
with open(file_path) as source_stream, open(f'{directory}/.tmp.{filename}', 'w') as destination_stream:
43+
for line in source_stream:
44+
destination_stream.write(self._yoficate.text(text=line))
45+
46+
os.rename(file_path, f'{directory}/{filename}.bak.before-yofication')
47+
os.rename(f'{directory}/.tmp.{filename}', file_path)
48+
os.remove(f'{directory}/{filename}.bak.before-yofication')
49+
50+
51+
def create_cli_parser() -> argparse.ArgumentParser:
52+
parser = argparse.ArgumentParser(
53+
prog='md.language.yofication',
54+
description='Yofication program replaces cyrillic letter "е" with letter "ё" in word where it should be.',
55+
)
56+
parser.add_argument('file_path', nargs='*', help='file to yoficate')
57+
58+
# dictionary_group = parser.add_mutually_exclusive_group()
59+
# dictionary_group.add_argument('--locale', action='store', choices=['ru_RU'], default='ru_RU')
60+
# dictionary_group.add_argument('--dictionary', action='store', default=None)
61+
62+
replace_group = parser.add_mutually_exclusive_group()
63+
replace_group.add_argument(
64+
'--replace',
65+
action='store_true',
66+
default=False,
67+
help='Replaces files with yoficated content'
68+
)
69+
replace_group.add_argument(
70+
'--no-replace',
71+
action='store_true',
72+
default=True,
73+
help=(
74+
'DEFAULT: Prints yoficated file content instead of replace. '
75+
'Exits with error, if few `file_path` arguments passed.'
76+
)
77+
)
78+
return parser
79+
80+
81+
def main(arguments: typing.Sequence[str]) -> int:
82+
# | FILES | REPLACE | RESULT |
83+
# |-------|---------|-------------------|
84+
# | =0 | 0 | OK (STDIN/STDOUT) |
85+
# | =0 | 1 | ERROR |
86+
# | =1 | 0 | OK (STDOUT) |
87+
# | =1 | 1 | OK (REPLACE) |
88+
# | >1 | 0 | ERROR |
89+
# | >1 | 1 | OK (REPLACE) |
90+
91+
parser = create_cli_parser()
92+
parsed_arguments = parser.parse_args(arguments)
93+
locale: typing.Literal['ru_RU'] = 'ru_RU' # todo add support in further versions
94+
95+
file_path_count = len(parsed_arguments.file_path)
96+
97+
# validate
98+
if parsed_arguments.replace:
99+
if file_path_count == 0:
100+
print('Error: no files specified to replace', file=sys.stderr)
101+
return 1
102+
else:
103+
if file_path_count > 1:
104+
print('Error: using `--no-replace` option with many files makes no sense', file=sys.stderr)
105+
return 1
106+
107+
# act
108+
builtin_dictionary = md.language.yofication.get_builtin_dictionary(locale=locale)
109+
yoficate = md.language.yofication.DefaultYoficate(dictionary=builtin_dictionary)
110+
111+
if not parsed_arguments.replace:
112+
# todo consider to separate to `--interactive/-i` and `-`$
113+
application = ApplicationEchoLines(yoficate=yoficate)
114+
if file_path_count == 0:
115+
application.run(input_stream=sys.stdin)
116+
return 0
117+
118+
assert file_path_count == 1
119+
with open(parsed_arguments.file_path[0], 'r') as stream:
120+
application.run(input_stream=stream)
121+
return 0
122+
123+
assert parsed_arguments.replace and file_path_count >= 1
124+
application = ApplicationReplaceLines(yoficate=yoficate)
125+
application.run(file_path_list=parsed_arguments.file_path)
126+
return 0
127+
128+
129+
if __name__ == '__main__':
130+
exit_code = main(sys.argv[1:])
131+
exit(exit_code)

0 commit comments

Comments
 (0)