Skip to content

Commit

Permalink
Merge pull request #18 from Krutyi-4el/develop
Browse files Browse the repository at this point in the history
Version 0.9.0
  • Loading branch information
solaluset authored Aug 15, 2023
2 parents 7579c9d + a3c1f87 commit 16f0a9c
Show file tree
Hide file tree
Showing 18 changed files with 391 additions and 48 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Run tests
run: python dev-helper.py run-tests
env:
SKIP_VENV: 1
- name: Upload coverage
# PyPy data isn't reliable because it changes trace function
if: "!startsWith(matrix.python-version, 'pypy')"
Expand All @@ -48,5 +46,3 @@ jobs:
python-version: "3.11"
- name: Run checks
run: python dev-helper.py run-checks
env:
SKIP_VENV: 1
2 changes: 1 addition & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ exclude = build
check_untyped_defs = true
warn_unused_ignores = true
warn_redundant_casts = true
no_implicit_reexport = true
disallow_incomplete_defs = true
# no_implicit_reexport = true
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## Note: **(B)** = BREAKING, (pb) = potentially breaking

### Hint: use `https://github.com/Krutyi-4el/i18nice/compare/v<version 1 (older)>...v<version 2 (newer)>` to see full code difference between versions

### v0.9.0
- **(B)** Removed `default=` kwarg of `i18n.t()`. You can work around this change with a custom handler [like this](https://github.com/Krutyi-4el/i18nice/blob/01ed6bcd2234998b411f07c92c31639e719dbabb/i18n/tests/translation_tests.py#L147)
- Added docstrings to public API
- Added `__all__` to most files
- Added support for `None` as `fallback`
- (pb) Made memoization enabled by default
- Added `lock=` kwarg to `load_everything()`

### v0.8.1
- Added flake8 and mypy checks
- Enabled and ensured branch coverage
- Added `__all__` to packages
- Added more type hints
- Fixed static references not expanding
- Fixed strict pluralization with translation tuples
- Created `dev-helper.py` for pre-commit and GA
- Made `PythonLoader` throw more verbose error

### v0.8.0
- (pb) `i18n.get("filename_format")` will return `FilenameFormat` instead of string. You can access `template` attribute to get the original string. `set` will continue to work as usual.
- More flexible filename formats
- Type hints for public APIs
- New functions `load_everything` and `unload_everything`
- 100% coverage
- Minor optimizations

### v0.7.0
- Added static references feature
- Added full translation list support

### v0.6.2
- Added PyPI publishing

## Note: versions listed below aren't available on PyPI

### v0.6.0
- (pb) Switched to `yaml.BaseLoader`

### v0.5.1
- Improved memoization (again)

### v0.5.0
- Rewrote `PythonLoader`
- **(B)** Removed old `error_on_missing_*` settings
- Improved memoization
- Improved file loading and fixed bugs
- Improved exceptions
- Added `reload_everything()`
- **(B)** Removed deprecated `other` plural

### v0.4.0
- Trying to set inexistent setting will now raise KeyError
- Added custom functions
- Fixed settings not updating properly
- **(B)** Dropped Python 2
- Added `on_missing_*` hooks
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
I appreciate your desire to contribute to this project

Here's a basic instruction on how to do it:

1. Fork my repository on GitHub

2. Clone your fork locally:

`git clone https://github.com/<your username>/i18nice`

4. `cd` into the folder and install the helper:

`python dev-helper.py install`

4. Make changes to the code.
Don't forget to write tests that will cover your changes, add type hints and adhere to flake8 formatting standard.

5. Commit your changes with proper commit message.
Fix problems identified by the helper if there are any.
You can make several commits.

6. Push the changes and open pull request to my repository

If anything is unclear or you just want to report a bug/request a feature, feel free to open an issue.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

This library provides i18n functionality for Python 3 out of the box. The usage is mostly based on Rails i18n library.

[CHANGELOG](CHANGELOG.md)

[CONTRIBUTING](CONTRIBUTING.md)

## Installation

Just run
Expand Down Expand Up @@ -49,7 +53,7 @@ If you need full yaml functionalities, override it with a custom loader:

### Memoization

Setting the configuration value `enable_memoization` will disable reloading of files every time when searching for missing translation.
The configuration value `enable_memoization` (`True` by default) disables reloading of files every time when searching for missing translation.
When translations are loaded, they're always stored in memory, hence it does not affect how existing translations are accessed.

### Load everything
Expand All @@ -61,6 +65,9 @@ You can call it with locale argument to load only one locale.

`i18n.reload_everything()` is just a shortcut for `unload_everything()` followed by `load_everything()`.

For the best performance, you can pass `lock=True` to `load_everything()` to disable searching for missing translations completely.
It'll prevent slowdowns caused by missing translations, but you'll need to use `unload_everything()` to be able to load files again.

### Namespaces

#### File namespaces
Expand Down
45 changes: 26 additions & 19 deletions dev-helper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""
This script automates testing and code quality checks
It stashes unstaged changes before testing to ensure accurate results
It also automatically manages virtual environment and dependencies
Run "python dev-helper.py install" to install it as pre-commit hook in this repository
"""

import os
import sys
import stat
Expand All @@ -9,6 +16,8 @@

VENV_DIR = "dev_env"
REQUIREMENTS = "dev-requirements.txt"
ON_CI = bool(os.getenv("CI"))
SKIP_VENV = ON_CI or os.getenv("SKIP_VENV") == "1"


def get_root():
Expand All @@ -24,25 +33,23 @@ def get_root():

def ensure_venv(packages):
venv_dir = os.path.join(os.getcwd(), VENV_DIR)
if sys.prefix == venv_dir:
# already in venv
return
if os.getenv("SKIP_VENV") != "1":
if not os.path.isdir(venv_dir):
print("Creating virtual environment...")
subprocess.run((sys.executable, "-m", "venv", venv_dir), check=True)
python_path = os.path.join(
sysconfig.get_path("scripts", "venv", {"base": venv_dir}),
"python",
)
pip_install(python_path, packages)
sys.exit(
subprocess.run(
(python_path, sys.argv[0], *sys.argv[1:]),
).returncode,
)
else:
if sys.prefix == venv_dir or SKIP_VENV:
# already in venv or skipping
print("Installing packages...")
pip_install(sys.executable, packages)
return
if not os.path.isdir(venv_dir):
print("Creating virtual environment...")
subprocess.run((sys.executable, "-m", "venv", venv_dir), check=True)
python_path = os.path.join(
sysconfig.get_path("scripts", "venv", {"base": venv_dir}),
"python",
)
sys.exit(
subprocess.run(
(python_path, *sys.argv),
).returncode,
)


def pip_install(python, args):
Expand Down Expand Up @@ -149,7 +156,7 @@ def check_coverage():
return (
cov.report() == 100.0
# always succeed on GA
or os.getenv("SKIP_VENV") == "1"
or ON_CI
)


Expand Down
20 changes: 19 additions & 1 deletion i18n/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__all__ = ("set", "get")

from typing import Any
from importlib import reload as _reload

Expand Down Expand Up @@ -43,12 +45,20 @@
'namespace_delimiter': '.',
'plural_few': 5,
'skip_locale_root_data': False,
'enable_memoization': False,
"enable_memoization": True,
'argument_delimiter': '|'
}


def set(key: str, value: Any) -> None:
"""
Sets config value
:param key: Setting to set
:param value: New value
:raises KeyError: If `key` is not a valid key
"""

if key not in settings:
raise KeyError("Invalid setting: {0}".format(key))
elif key == 'load_path':
Expand All @@ -69,6 +79,14 @@ def set(key: str, value: Any) -> None:


def get(key: str) -> Any:
"""
Gets config value
:param key: Setting to get
:return: Associated value
:raises KeyError: If `key` is not a valid key
"""

return settings[key]


Expand Down
15 changes: 15 additions & 0 deletions i18n/custom_functions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__all__ = ("add_function", "get_function")

from collections import defaultdict
from typing import Optional, Callable, Dict

Expand All @@ -8,6 +10,19 @@


def add_function(name: str, func: Function, locale: Optional[str] = None) -> None:
"""
Adds your function to placeholder functions
Registers the function to locale functions if locale is given
or to global (locale-independent) functions otherwise
The function must accept all kwargs passed to `t` and return an int
(index that will determine which placeholder argument will be used)
:param name: Name used to register the function
:param func: The function to register
:param locale: Locale to which function will be bound (optional)
"""

if locale:
locales_functions[locale][name] = func
else:
Expand Down
10 changes: 10 additions & 0 deletions i18n/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class I18nException(Exception):
"""Base class for i18n errors"""

def __init__(self, value: str):
self.value = value

Expand All @@ -7,12 +9,20 @@ def __str__(self):


class I18nFileLoadError(I18nException):
"""Raised when file load fails"""
pass


class I18nInvalidStaticRef(I18nException):
"""Raised when static reference cannot be resolved"""
pass


class I18nInvalidFormat(I18nException):
"""Raised when provided filename_format is invalid"""
pass


class I18nLockedError(I18nException):
"""Raised when trying to load locked translations"""
pass
2 changes: 2 additions & 0 deletions i18n/formatters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__all__ = ("TranslationFormatter", "StaticFormatter", "FilenameFormat", "expand_static_refs")

from re import compile, escape
try:
from re import Match
Expand Down
3 changes: 2 additions & 1 deletion i18n/loaders/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__all__: tuple = ("Loader", "PythonLoader", "I18nFileLoadError")

from .loader import Loader, I18nFileLoadError
from .loader import Loader
from ..errors import I18nFileLoadError
from .python_loader import PythonLoader
from .. import config
if config.json_available:
Expand Down
44 changes: 44 additions & 0 deletions i18n/loaders/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ def __init__(self):
super(Loader, self).__init__()

def load_file(self, filename: str) -> str:
"""
Reads content from file
:param filename: The file to read
:return: Content of the file
:raises I18nFileLoadError: If loading wasn't successful
"""

try:
with io.open(filename, 'r', encoding=config.get('encoding')) as f:
return f.read()
Expand All @@ -24,16 +32,40 @@ def load_file(self, filename: str) -> str:
) from e

def parse_file(self, file_content: str) -> dict:
"""
Parses file content to dict. Must be implemented in subclasses
:param file_content: Content of the file
:return: Parsed content
:raises I18nFileLoadError: If parsing wasn't successful
"""

raise NotImplementedError(
"the method parse_file has not been implemented for class {0}".format(
self.__class__.__name__,
),
)

def check_data(self, data: dict, root_data: Optional[str]) -> bool:
"""
Checks if `root_data` is present in the content
:param data: Parsed content of the file
:param root_data: Data element to be checked. If `None`, check always succeeds
:return: `True` if `root_data` is present, `False` otherwise
"""

return True if root_data is None else root_data in data

def get_data(self, data: dict, root_data: Optional[str]) -> dict:
"""
Extracts `root_data` from `data`
:param data: Full parsed data
:param root_data: Part of the data to extract
:return: Extracted data
"""

# use .pop to remove used data from cache
return data if root_data is None else data.pop(root_data)

Expand All @@ -43,6 +75,18 @@ def load_resource(
root_data: Optional[str],
remember_content: bool,
) -> dict:
"""
Main function for resource loading
Manages caching
:param filename: File to load
:param root_data: Part of data to extract
:param remember_content: Whether to save other parts of data in cache
:return: Loaded data
:raises I18nFileLoadError: If loading wasn't successful
"""

filename = os.path.abspath(filename)
if filename in self.loaded_files:
data = self.loaded_files[filename]
Expand Down
Loading

0 comments on commit 16f0a9c

Please sign in to comment.