Improve hatch scripts and create executables

This commit is contained in:
Sina Atalay
2025-02-04 14:16:39 -05:00
parent de24974c65
commit 0ddd489951
26 changed files with 300 additions and 167 deletions

View File

@@ -14,6 +14,34 @@ jobs:
name: Run Tests
uses: ./.github/workflows/test.yaml
create_executables:
name: Create Executables
strategy:
fail-fast: false
matrix:
os: [ubuntu, windows, macos]
runs-on: ${{ matrix.os }}-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install Hatch
uses: pypa/hatch@install
- name: Create executable
run: |
hatch run create-executable
- name: Upload executable
uses: softprops/action-gh-release@v2
with:
files: |
bin/*
publish_to_pypi:
name: Publish to PyPI
needs:
@@ -37,9 +65,6 @@ jobs:
run: |
hatch build
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Upload wheel and source distribution
uses: softprops/action-gh-release@v2
with:
@@ -47,6 +72,9 @@ jobs:
dist/*.whl
dist/*.tar.gz
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
publish_to_dockerhub:
name: Publish to Docker Hub
needs:

View File

@@ -27,14 +27,14 @@ jobs:
- name: Update schema.json
continue-on-error: true
run: |
hatch run docs:update-schema
hatch run update-schema
git add schema.json
git commit -m "docs: update schema.json"
- name: Update `examples` folder
continue-on-error: true
run: |
hatch run docs:update-examples
hatch run update-examples
git add examples/*
git add docs/assets/images/*.png
git commit -m "docs: update examples"

5
.gitignore vendored
View File

@@ -186,4 +186,7 @@ rendercv_output/
.DS_Store
# Profiling:
render_command.prof
render_command.prof
# Executables:
bin/

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -56,6 +56,10 @@ This is done with [Development containers](https://containers.dev/), and the env
These commands are defined in the [`pyproject.toml`](https://github.com/rendercv/rendercv/blob/main/pyproject.toml) file.
- Build the package
```bash
hatch run build
```
- Format the code with [Black](https://github.com/psf/black) and [Ruff](https://github.com/astral-sh/ruff)
```bash
hatch run format
@@ -80,6 +84,18 @@ These commands are defined in the [`pyproject.toml`](https://github.com/rendercv
```bash
hatch run test-and-report
```
- Update [schema.json](https://github.com/rendercv/rendercv/blob/main/schema.json)
```bash
hatch run update-schema
```
- Update [`examples`](https://github.com/rendercv/rendercv/tree/main/examples) folder
```bash
hatch run update-examples
```
- Create an executable version of RenderCV with [PyInstaller](https://www.pyinstaller.org/)
```bash
hatch run create-executables
```
- Preview the documentation as you write it
```bash
hatch run docs:serve
@@ -88,14 +104,6 @@ These commands are defined in the [`pyproject.toml`](https://github.com/rendercv
```bash
hatch run docs:build
```
- Update [schema.json](https://github.com/rendercv/rendercv/blob/main/schema.json)
```bash
hatch run docs:update-schema
```
- Update [`examples`](https://github.com/rendercv/rendercv/tree/main/examples) folder
```bash
hatch run docs:update-examples
```
- Update figures of the entry types in the "[Structure of the YAML Input File](../user_guide/structure_of_the_yaml_input_file.md)"
```bash
hatch run docs:update-entry-figures

View File

@@ -13,12 +13,12 @@ Once the changes are pushed to the `main` branch, the [`deploy-docs.yaml`](https
The `examples` folder includes example YAML files for all the built-in themes, along with their corresponding PDF outputs. Also, there are PNG files of the first pages of each theme in [`docs/assets/images`](https://github.com/rendercv/rendercv/tree/main/docs/assets/images). These examples are shown in [`README.md`](https://github.com/rendercv/rendercv/blob/main/README.md).
These files are generated using [`docs/update_examples.py`](https://github.com/rendercv/rendercv/blob/main/docs/update_examples.py). The contents of the examples are taken from the [`create_a_sample_data_model`](https://docs.rendercv.com/reference/data/#rendercv.data.create_a_sample_data_model) function from [`rendercv.data`](https://docs.rendercv.com/reference/data/).
These files are generated using [`scripts/update_examples.py`](https://github.com/rendercv/rendercv/blob/main/scripts/update_examples.py). The contents of the examples are taken from the [`create_a_sample_data_model`](https://docs.rendercv.com/reference/data/#rendercv.data.create_a_sample_data_model) function from [`rendercv.data`](https://docs.rendercv.com/reference/data/).
Run the following command to update the `examples` folder.
```bash
hatch run docs:update-examples
hatch run update-examples
```
Once a new release is created on GitHub, the [`publish-to-pypi.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/publish-to-pypi.yaml) workflow will be automatically triggered, and the `examples` folder will be updated to the most recent version.
@@ -27,7 +27,7 @@ Once a new release is created on GitHub, the [`publish-to-pypi.yaml`](https://gi
There are example figures for each entry type for each theme in the "[Structure of the YAML Input File](../user_guide/structure_of_the_yaml_input_file.md)" page.
The figures are generated using [`docs/update_entry_figures.py`](https://github.com/rendercv/rendercv/blob/main/docs/update_entry_figures.py).
The figures are generated using [`scripts/update_entry_figures.py`](https://github.com/rendercv/rendercv/blob/main/scripts/update_entry_figures.py).
Run the following command to update the figures.
@@ -41,12 +41,12 @@ Once a new release is created on GitHub, the [`publish-to-pypi.yaml`](https://gi
The schema of RenderCV's input file is defined using [Pydantic](https://docs.pydantic.dev/latest/). Pydantic allows automatic creation and customization of JSON schemas from Pydantic models.
The JSON Schema is also generated using [`docs/update_schema.py`](https://github.com/rendercv/rendercv/blob/main/docs/update_schema.py). It uses [`generate_json_schema`](https://docs.rendercv.com/reference/data/#rendercv.data.generate_json_schema) function from [`rendercv.data`](https://docs.rendercv.com/reference/data/).
The JSON Schema is also generated using [`scripts/update_schema.py`](https://github.com/rendercv/rendercv/blob/main/scripts/update_schema.py). It uses [`generate_json_schema`](https://docs.rendercv.com/reference/data/#rendercv.data.generate_json_schema) function from [`rendercv.data`](https://docs.rendercv.com/reference/data/).
Run the following command to update the JSON Schema.
```bash
hatch run docs:update-schema
hatch run update-schema
```
Once a new release is created on GitHub, the [`publish-to-pypi.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/publish-to-pypi.yaml) workflow will be automatically triggered, and `schema.json` will be updated to the most recent version.

View File

@@ -0,0 +1,150 @@
"""This script generates the example entry figures and creates an environment for
documentation templates using `mkdocs-macros-plugin`. For example, the content of the
example entries found in
"[Structure of the YAML Input File](https://docs.rendercv.com/user_guide/structure_of_the_yaml_input_file/)"
are coming from this script.
"""
import io
import pathlib
from typing import get_args
import pydantic
import ruamel.yaml
import rendercv.data as data
import rendercv.themes.options as theme_options
repository_root = pathlib.Path(__file__).parent.parent
rendercv_path = repository_root / "rendercv"
image_assets_directory = pathlib.Path(__file__).parent / "assets" / "images"
class SampleEntries(pydantic.BaseModel):
education_entry: data.EducationEntry
experience_entry: data.ExperienceEntry
normal_entry: data.NormalEntry
publication_entry: data.PublicationEntry
one_line_entry: data.OneLineEntry
bullet_entry: data.BulletEntry
numbered_entry: data.NumberedEntry
reversed_numbered_entry: data.ReversedNumberedEntry
text_entry: str
def dictionary_to_yaml(dictionary: dict):
"""Converts a dictionary to a YAML string.
Args:
dictionary: The dictionary to be converted to YAML.
Returns:
The YAML string.
"""
yaml_object = ruamel.yaml.YAML()
yaml_object.width = 60
yaml_object.indent(mapping=2, sequence=4, offset=2)
with io.StringIO() as string_stream:
yaml_object.dump(dictionary, string_stream)
return string_stream.getvalue()
def define_env(env):
# See https://mkdocs-macros-plugin.readthedocs.io/en/latest/macros/
sample_entries = data.read_a_yaml_file(
repository_root / "docs" / "user_guide" / "sample_entries.yaml"
)
# validate the parsed dictionary by creating an instance of SampleEntries:
SampleEntries(**sample_entries)
entries_showcase = {}
for entry_name, entry in sample_entries.items():
proper_entry_name = entry_name.replace("_", " ").title().replace(" ", "")
entries_showcase[proper_entry_name] = {
"yaml": dictionary_to_yaml(entry),
"figures": [
{
"path": f"../assets/images/{theme}/{entry_name}.png",
"alt_text": f"{proper_entry_name} in {theme}",
"theme": theme,
}
for theme in data.available_themes
],
}
env.variables["sample_entries"] = entries_showcase
# For theme templates reference docs
themes_path = rendercv_path / "themes"
theme_templates = {}
for theme in data.available_themes:
theme_templates[theme] = {}
for theme_file in themes_path.glob(f"{theme}/*.typ"):
theme_templates[theme][theme_file.stem] = theme_file.read_text()
# Update ordering of theme templates
order = [
"Preamble.j2",
"Header.j2",
"SectionBeginning.j2",
"SectionEnding.j2",
"TextEntry.j2",
"BulletEntry.j2",
"OneLineEntry.j2",
"EducationEntry.j2",
"ExperienceEntry.j2",
"NormalEntry.j2",
"PublicationEntry.j2",
]
theme_templates[theme] = {key: theme_templates[theme][key] for key in order}
if theme != "markdown":
theme_templates[theme] = {
f"{key}.typ": value for key, value in theme_templates[theme].items()
}
else:
theme_templates[theme] = {
f"{key}.md": value for key, value in theme_templates[theme].items()
}
env.variables["theme_templates"] = theme_templates
# Available themes strings (put available themes between ``)
themes = [f"`{theme}`" for theme in data.available_themes]
env.variables["available_themes"] = ", ".join(themes)
# Available social networks strings (put available social networks between ``)
social_networks = [
f"`{social_network}`" for social_network in data.available_social_networks
]
env.variables["available_social_networks"] = ", ".join(social_networks)
# Others:
env.variables["available_page_sizes"] = ", ".join(
[f"`{page_size}`" for page_size in get_args(theme_options.PageSize)]
)
env.variables["available_font_families"] = ", ".join(
[f"`{font_family}`" for font_family in get_args(theme_options.FontFamily)]
)
env.variables["available_text_alignments"] = ", ".join(
[
f"`{text_alignment}`"
for text_alignment in get_args(theme_options.TextAlignment)
]
)
env.variables["available_header_alignments"] = ", ".join(
[
f"`{header_alignment}`"
for header_alignment in get_args(theme_options.Alignment)
]
)
env.variables["available_section_title_types"] = ", ".join(
[
f"`{section_title_type}`"
for section_title_type in get_args(
get_args(theme_options.SectionTitleType)[0]
)
]
)
env.variables["available_bullets"] = ", ".join(
[f"`{bullet}`" for bullet in get_args(theme_options.BulletPoint)]
)

View File

@@ -124,7 +124,7 @@ markdown_extensions:
plugins:
- search
- macros: # mkdocs-macros-plugin
module_name: docs/update_entry_figures
module_name: docs/dynamic_content_generation
- mkdocstrings:
handlers:
python:

View File

@@ -126,15 +126,16 @@ rendercv = 'rendercv.cli:app'
installer = "uv"
python = "3.13"
dependencies = [
"ruff", # to lint and format the code
"black", # to format the code
"ipython", # for ipython shell
"pyright", # to check the types
"pre-commit", # to run the checks before committing
"pytest==8.3.4", # to run the tests
"coverage==7.6.10", # to generate coverage reports
"pypdf==5.1.0", # to read PDF files
"snakeviz==2.2.2", # for profiling
"ruff", # to lint and format the code
"black", # to format the code
"ipython", # for ipython shell
"pyright", # to check the types
"pre-commit", # to run the checks before committing
"pytest==8.3.4", # to run the tests
"coverage==7.6.10", # to generate coverage reports
"pypdf==5.1.0", # to read PDF files
"snakeviz==2.2.2", # for profiling
"pyinstaller==6.11.1", # to build the executable
]
features = ["full"] # to install full optional dependencies
[tool.hatch.envs.default.scripts]
@@ -154,6 +155,12 @@ test = "pytest" # hatch run test
test-and-report = "coverage run -m pytest && coverage combine && coverage report && coverage html --show-contexts" # hatch run test-and-report
# Profile render command
profile-render-command = "python -m cProfile -o render_command.prof -m rendercv render examples/John_Doe_ClassicTheme_CV.yaml && snakeviz render_command.prof"
# Update schema.json:
update-schema = "python scripts/update_schema.py" # hatch run update-schema
# Update `examples` folder:
update-examples = "python scripts/update_examples.py" # hatch run update-examples
# Create executables
create-executable = "python scripts/create_executable.py" # hatch run create-executables
[tool.hatch.envs.test]
template = "default"
@@ -178,12 +185,8 @@ features = ["full"] # to install full optional dependencies
build = "mkdocs build --clean --strict" # hatch run docs:build
# Start the development server for the documentation with `mkdocs`:
serve = "mkdocs serve" # hatch run docs:serve
# Update schema.json:
update-schema = "python docs/update_schema.py" # hatch run docs:update-schema
# Update `examples` folder:
update-examples = "python docs/update_examples.py" # hatch run docs:update-examples
# Update entry figures in "Structure of the YAML File" page:
update-entry-figures = "python docs/update_entry_figures.py" # hatch run docs:update-entry-figures
update-entry-figures = "python scripts/update_entry_figures.py" # hatch run docs:update-entry-figures
# ======================================================================================
# Virtual Environments Above ===========================================================
# ======================================================================================

View File

@@ -0,0 +1,62 @@
import os
import pathlib
import subprocess
import sys
def create_executable():
# Make sure the current working directory is the root of the project:
root = pathlib.Path(__file__).parent.parent
os.chdir(root)
rendercv_file_path = root / "rendercv.py"
rendercv_file_path.touch()
rendercv_file_path.write_text("import rendercv.cli as cli; cli.app()")
# Run pyinstaller:
subprocess.run(
[
sys.executable,
"-m",
"PyInstaller",
"--onefile",
"--clean",
"--collect-all",
"rendercv",
"--collect-all",
"rendercv_fonts",
"--distpath",
"bin",
str(rendercv_file_path),
],
check=True,
)
# Remove the temporary file:
rendercv_file_path.unlink()
(root / "rendercv.spec").unlink()
# Rename the executable:
platform = {
"linux": "linux",
"darwin": "macos",
"win32": "windows",
}
if sys.platform == "win32":
executable_path = root / "bin" / "rendercv.exe"
executable_path.rename(
root
/ "bin"
/ f"rendercv-{platform[sys.platform]}-{os.environ['PROCESSOR_ARCHITECTURE']}"
)
else:
executable_path = root / "bin" / "rendercv"
executable_path.rename(
root / "bin" / f"rendercv-{platform[sys.platform]}-{os.uname().machine}"
)
print('Executable created at "bin" folder.') # NOQA: T201
if __name__ == "__main__":
create_executable()

View File

@@ -5,24 +5,20 @@ example entries found in
are coming from this script.
"""
import io
import pathlib
import shutil
import tempfile
from typing import get_args
import fitz
import pdfCropMargins
import pydantic
import ruamel.yaml
import rendercv.data as data
import rendercv.renderer as renderer
import rendercv.themes.options as theme_options
repository_root = pathlib.Path(__file__).parent.parent
rendercv_path = repository_root / "rendercv"
image_assets_directory = pathlib.Path(__file__).parent / "assets" / "images"
image_assets_directory = repository_root / "docs" / "assets" / "images"
class SampleEntries(pydantic.BaseModel):
@@ -37,124 +33,6 @@ class SampleEntries(pydantic.BaseModel):
text_entry: str
def dictionary_to_yaml(dictionary: dict):
"""Converts a dictionary to a YAML string.
Args:
dictionary: The dictionary to be converted to YAML.
Returns:
The YAML string.
"""
yaml_object = ruamel.yaml.YAML()
yaml_object.width = 60
yaml_object.indent(mapping=2, sequence=4, offset=2)
with io.StringIO() as string_stream:
yaml_object.dump(dictionary, string_stream)
return string_stream.getvalue()
def define_env(env):
# See https://mkdocs-macros-plugin.readthedocs.io/en/latest/macros/
sample_entries = data.read_a_yaml_file(
repository_root / "docs" / "user_guide" / "sample_entries.yaml"
)
# validate the parsed dictionary by creating an instance of SampleEntries:
SampleEntries(**sample_entries)
entries_showcase = {}
for entry_name, entry in sample_entries.items():
proper_entry_name = entry_name.replace("_", " ").title().replace(" ", "")
entries_showcase[proper_entry_name] = {
"yaml": dictionary_to_yaml(entry),
"figures": [
{
"path": f"../assets/images/{theme}/{entry_name}.png",
"alt_text": f"{proper_entry_name} in {theme}",
"theme": theme,
}
for theme in data.available_themes
],
}
env.variables["sample_entries"] = entries_showcase
# For theme templates reference docs
themes_path = rendercv_path / "themes"
theme_templates = {}
for theme in data.available_themes:
theme_templates[theme] = {}
for theme_file in themes_path.glob(f"{theme}/*.typ"):
theme_templates[theme][theme_file.stem] = theme_file.read_text()
# Update ordering of theme templates
order = [
"Preamble.j2",
"Header.j2",
"SectionBeginning.j2",
"SectionEnding.j2",
"TextEntry.j2",
"BulletEntry.j2",
"OneLineEntry.j2",
"EducationEntry.j2",
"ExperienceEntry.j2",
"NormalEntry.j2",
"PublicationEntry.j2",
]
theme_templates[theme] = {key: theme_templates[theme][key] for key in order}
if theme != "markdown":
theme_templates[theme] = {
f"{key}.typ": value for key, value in theme_templates[theme].items()
}
else:
theme_templates[theme] = {
f"{key}.md": value for key, value in theme_templates[theme].items()
}
env.variables["theme_templates"] = theme_templates
# Available themes strings (put available themes between ``)
themes = [f"`{theme}`" for theme in data.available_themes]
env.variables["available_themes"] = ", ".join(themes)
# Available social networks strings (put available social networks between ``)
social_networks = [
f"`{social_network}`" for social_network in data.available_social_networks
]
env.variables["available_social_networks"] = ", ".join(social_networks)
# Others:
env.variables["available_page_sizes"] = ", ".join(
[f"`{page_size}`" for page_size in get_args(theme_options.PageSize)]
)
env.variables["available_font_families"] = ", ".join(
[f"`{font_family}`" for font_family in get_args(theme_options.FontFamily)]
)
env.variables["available_text_alignments"] = ", ".join(
[
f"`{text_alignment}`"
for text_alignment in get_args(theme_options.TextAlignment)
]
)
env.variables["available_header_alignments"] = ", ".join(
[
f"`{header_alignment}`"
for header_alignment in get_args(theme_options.Alignment)
]
)
env.variables["available_section_title_types"] = ", ".join(
[
f"`{section_title_type}`"
for section_title_type in get_args(
get_args(theme_options.SectionTitleType)[0]
)
]
)
env.variables["available_bullets"] = ", ".join(
[f"`{bullet}`" for bullet in get_args(theme_options.BulletPoint)]
)
def render_pngs_from_pdf(pdf_file_path: pathlib.Path) -> list[pathlib.Path]:
"""Render a PNG file for each page of the given PDF file.

View File

@@ -10,7 +10,7 @@ import rendercv.renderer as renderer
repository_root = pathlib.Path(__file__).parent.parent
rendercv_path = repository_root / "rendercv"
image_assets_directory = pathlib.Path(__file__).parent / "assets" / "images"
image_assets_directory = repository_root / "docs" / "assets" / "images"
def generate_examples():

View File

@@ -26,7 +26,6 @@ from rendercv.renderer import templater
# reference files with the latest output.
update_testdata = False
# copy sample entries from docs/update_rendercv_files.py:
education_entry_dictionary = {
"institution": "Boğaziçi University",
"location": "Istanbul, Turkey",

View File

@@ -1,5 +1,4 @@
import subprocess
import sys
import pytest
@@ -9,16 +8,19 @@ import pytest
[
"format",
"lint",
"sort-imports",
"check-types",
"precommit",
"update-schema",
"update-examples",
"create-executable",
"docs:build",
"docs:update-schema",
"docs:update-examples",
"docs:update-entry-figures",
# "docs:serve",
# "test:run",
# "test:run-and-report",
],
)
def test_default_format(script_name):
subprocess.run([sys.executable, "-m", "hatch", "run", script_name], check=False)
# Check if hatch is installed:
try:
subprocess.run(["hatch", "--version"], check=True)
subprocess.run(["hatch", "run", script_name], check=True)
except FileNotFoundError:
pass