Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@
**Decision:**
- **Safety-First Optimization:** The optimization now only skips style application if both the intended style is `Default` AND the document's default is also `Default`. If a custom `default_cell_style` (like `Normal`) is defined, it will always be explicitly applied to ensure document consistency.

### 3. Regression Testing
### 5. Localization Workflow (`poe translate`)
**Observation:** The project uses `poe translate` (linked to `unogenerator.poethepoet:translate`) to automate the synchronization of `.po` files with the `.pot` template.
**Experience:**
- **English Redundancy:** `en.po` has been removed as it was redundant. Since the source strings in the code are in English, the system now uses `fallback=True` in `gettext.translation` calls within `demo.py`. This ensures that when the English locale is requested, it falls back to the original English strings without needing a separate `.po` file.
- **Header Fragility:** Running `poe translate` may overwrite manual header improvements or reset them to defaults.
- **Multilingual Support:** While efficient for syncing Spanish (the developer's primary language), it might leave other languages (French, Romanian) with empty `msgstr` entries if not carefully monitored.
- **Verification:** After running `poe translate`, always verify that all message strings in all supported languages are correctly populated and that headers maintain the correct project metadata and dates. Manual restoration of translations for non-primary languages may be required if the tool only targets one language.

### 6. Regression Testing
New tests have been added to `tests/test_unogenerator.py` to protect these fixes:
- `test_ods_row_height_consistency`: Ensures heights stay at 452 even after `setColumnsWidth` with many columns.
- `test_ods_normal_style_applied`: Verifies that `Normal` style is correctly applied by `ODS_Standard`.
Expand All @@ -27,3 +35,21 @@ New tests have been added to `tests/test_unogenerator.py` to protect these fixes
**Decision:**
- **Explicit Dependency:** `envwrap` has been added to `pyproject.toml`. While not a direct dependency of the library's core logic, it is essential for the environment's stability when `tqdm` and `uno` coexist.
- **Safety:** It is a safe, lightweight utility for environment variable wrapping. Adding it explicitly prevents the intermittent `ImportError` and ensures that tests and demo scripts run reliably across different setups.

### 7. PyUNO Initialization in Concurrent Processes
**Problem:** When using `multiprocessing` with the `spawn` start method (as used in the demo), child processes would fail with `SystemError: pyuno runtime is not initialized`. This is because `uno.getComponentContext()` (from the standard `uno.py`) returns a cached context object that is initialized during the module's import phase. In `spawn` mode, this initialization happens during the child process's bootstrap and is not valid for the subsequent execution phase.

**Decision:**
- **Direct PyUNO Access:** We now use `pyuno.getComponentContext()` directly instead of the cached `uno.getComponentContext()`.
- **Deferred Initialization:** In `unogenerator.py`, we defined a local `getComponentContext()` wrapper that calls the underlying `pyuno` function. This ensures that every time the context is requested (e.g., during `ODF.__init__` or `createUnoService`), `pyuno` performs a fresh initialization check in the current process phase.
- **Verification:** A new test `tests/test_concurrency.py` has been added to specifically verify that `spawn`-ed processes can successfully initialize and use UNO objects.

### 8. Optimized Thread-Safety via Instance-Based Serialization
**Problem:** Running the demo with concurrent threads and a shared LibreOffice server (`COMMONSERVER_CONCURRENT_THREADS`) was unstable due to non-thread-safe sharing of a single connection. However, using a single global lock serialized execution even for workers using independent LibreOffice instances, causing unnecessary performance loss.

**Decision:**
- **Instance-Based Reentrant Lock:** Each `LibreofficeServer` object now maintains its own `threading.RLock()`.
- **Smart Synchronization:** The `@uno_safe` class decorator dynamically retrieves the lock from the document's associated server instance (`self.server._lock`).
- **Parallel Performance:** This allows workers using independent servers (different ports) to run at full parallel speed without waiting for each other.
- **Shared-Server Stability:** Workers sharing the same `LibreofficeServer` instance will correctly share the same lock, ensuring serialized access to the single connection and maintaining stability.
- **Internal Protection:** Global UNO bridge calls (like `getComponentContext`) remain protected by a global `_uno_bridge_lock` to ensure the process-wide PyUNO state is not corrupted during initialization.
Binary file modified doc/unogenerator_documentation_en.odt
Binary file not shown.
Binary file modified doc/unogenerator_documentation_en.pdf
Binary file not shown.
Binary file modified doc/unogenerator_documentation_es.odt
Binary file not shown.
Binary file modified doc/unogenerator_documentation_es.pdf
Binary file not shown.
Binary file modified doc/unogenerator_example_en.ods
Binary file not shown.
Binary file modified doc/unogenerator_example_en.pdf
Binary file not shown.
Binary file modified doc/unogenerator_example_es.ods
Binary file not shown.
Binary file modified doc/unogenerator_example_es.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "unogenerator"
version = "1.3.0"
version = "1.4.0"
description = "Libreoffice files generator programmatically with python and Libreoffice server instances"
authors = [
{ name = "turulomio", email = "turulomio@yahoo.es" }
Expand Down
4 changes: 2 additions & 2 deletions unogenerator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
logger.addHandler(logging.NullHandler()) # Add NullHandler to prevent "No handlers could be found" message
logger.setLevel(logging.WARNING) # Set default level for the library to WARNING

__version__ = '1.3.0'
__versiondatetime__=datetime(2026, 5, 24, 9, 52)
__version__ = '1.4.0'
__versiondatetime__=datetime(2026, 6, 7, 4, 20)
__versiondate__=__versiondatetime__.date()


Expand Down
36 changes: 16 additions & 20 deletions unogenerator/demo.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
from uno import getComponentContext

getComponentContext()
import argparse
from collections import OrderedDict
from concurrent.futures import ProcessPoolExecutor, as_completed, ThreadPoolExecutor
from datetime import datetime, date, timedelta
from gettext import translation # Removed 'info'
import logging # Import logging module
from importlib.resources import files
from os import system
from subprocess import run
from pydicts.currency import Currency
from pydicts.percentage import Percentage
from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer, helpers, types, Range
Expand All @@ -20,8 +17,6 @@
except:
_=str

type_choices=[ "SEQUENTIAL", "CONCURRENT_PROCESS", "CONCURRENT_THREADS", "COMMONSERVER_SEQUENTIAL","COMMONSERVER_CONCURRENT_PROCESS","COMMONSERVER_CONCURRENT_THREADS" ]

## If arguments is None, launches with sys.argc parameters. Entry point is toomanyfiles:main

logger = logging.getLogger(__name__) # Get logger for this module
Expand Down Expand Up @@ -68,28 +63,29 @@



## You can call with main(['--pretend']). It's equivalento to os.system('program --pretend')
## You can call with main(['--pretend']). It's equivalento to run(['program', '--pretend'])
## @param arguments is an array with parser arguments. For example: ['--argument','9'].
def demo(arguments=None):

parser=argparse.ArgumentParser(prog='unogenerator', description=_('Create example files using unogenerator module'), epilog=commons.argparse_epilog(), formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--version', action='version', version=__version__)
parser.add_argument('--debug', help=_("Debug program information"), choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"], default="ERROR")
group= parser.add_mutually_exclusive_group(required=True)
group.add_argument('--create', help="Create demo files", action="store_true",default=False)
group.add_argument('--remove', help="Remove demo files", action="store_true", default=False)
group.add_argument('--benchmark', help="Executes all types to compare its benchmark", action="store_true", default=False)
parser.add_argument('--type', help="Debug program information", choices=type_choices, default="COMMONSERVER_CONCURRENT_PROCESS")
parser.add_argument('--type', help="Debug program information", choices=[t.name for t in types.DemoType], default=types.DemoType.CONCURRENT_PROCESS.name)
args=parser.parse_args(arguments)
commons.addDebugSystem(args.debug)
demo_command(args.create, args.remove, args.benchmark, args.type)
demo_command(args.create, args.remove, args.benchmark, types.DemoType[args.type])

def demo_command(create, remove, benchmark, type):
languages=['es', 'en', 'ro', 'fr']

if benchmark is True:
for type in type_choices:
for t in types.DemoType:
#demo_command(True, False, False, type)
system(f"unogenerator_demo --create --type {type}")
run(["unogenerator_demo", "--create", "--type", t.name], check=True)

if remove==True:
for language in languages:
Expand All @@ -102,10 +98,10 @@ def demo_command(create, remove, benchmark, type):

if create==True:
start=datetime.now()
instances=3
instances=4
total_documents=len(languages)*2

if type=="CONCURRENT_PROCESS":
if type==types.DemoType.CONCURRENT_PROCESS:
futures=[]
print(_("Launching demo with {0} workers without common server using concurrent processes").format(instances))

Expand All @@ -127,7 +123,7 @@ def demo_command(create, remove, benchmark, type):
result = future.result()
results.append(result)

elif type=="COMMONSERVER_CONCURRENT_PROCESS":
elif type==types.DemoType.COMMONSERVER_CONCURRENT_PROCESS:
futures=[]
print(_("Launching demo with {0} workers with common server using concurrent processes").format(instances))

Expand Down Expand Up @@ -160,7 +156,7 @@ def demo_command(create, remove, benchmark, type):
finally:
main_server.stop() # Ensure the main server is stopped when done

elif type=="CONCURRENT_THREADS":
elif type==types.DemoType.CONCURRENT_THREADS:
futures=[]
print(_("Launching demo with {0} workers without common server using concurrent threads").format(instances))

Expand All @@ -182,7 +178,7 @@ def demo_command(create, remove, benchmark, type):
result = future.result()
results.append(result)

elif type=="COMMONSERVER_CONCURRENT_THREADS":
elif type==types.DemoType.COMMONSERVER_CONCURRENT_THREADS:
futures=[]
print(_("Launching demo with {0} workers with common server using concurrent threads").format(instances))

Expand All @@ -205,7 +201,7 @@ def demo_command(create, remove, benchmark, type):
result = future.result()
results.append(result)

elif type=="COMMONSERVER_SEQUENTIAL":
elif type==types.DemoType.COMMONSERVER_SEQUENTIAL:
with LibreofficeServer() as server:
print(_("Launching demo with one common server sequentially"))
with tqdm(total=total_documents) as progress:
Expand All @@ -215,7 +211,7 @@ def demo_command(create, remove, benchmark, type):
demo_odt_standard(language, server)
progress.update()

elif type=="SEQUENTIAL":
elif type==types.DemoType.SEQUENTIAL:
print(_("Launching demo without one common server sequentially"))
with tqdm(total=total_documents) as progress:
for language in languages:
Expand All @@ -228,7 +224,7 @@ def demo_command(create, remove, benchmark, type):


def demo_ods_standard(language, server):
lang1=translation('unogenerator', files("unogenerator") / 'locale', languages=[language])
lang1=translation('unogenerator', files("unogenerator") / 'locale', languages=[language], fallback=True)
lang1.install()
_=lang1.gettext

Expand Down Expand Up @@ -264,7 +260,7 @@ def demo_ods_standard(language, server):


def demo_odt_standard(language, server):
lang1=translation('unogenerator', files("unogenerator") / 'locale', languages=[language])
lang1=translation('unogenerator', files("unogenerator") / 'locale', languages=[language], fallback=True)
lang1.install()
_=lang1.gettext

Expand Down
20 changes: 0 additions & 20 deletions unogenerator/locale/en.po

This file was deleted.

Binary file removed unogenerator/locale/en/LC_MESSAGES/unogenerator.mo
Binary file not shown.
24 changes: 14 additions & 10 deletions unogenerator/locale/es.po
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
# Spanish translations for unogenerator package
# Traducciones al español para el paquete unogenerator.
# Copyright (C) 2024 turulomio
# Copyright (C) 2020-2026 Mariano Muñoz
# This file is distributed under the same license as the unogenerator package.
# Mariano Muñoz <turulomio@yahoo.es>, 2015-2026.
#
# Mariano Muñoz <turulomio@yahoo.es>, 2015, 2020, 2024.
msgid ""
msgstr ""
"Project-Id-Version: Unogenerator\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-24 09:56+0200\n"
"PO-Revision-Date: 2024-07-31 12:00:00+0200\n"
"Last-Translator: turulomio@yahoo.es\n"
"Language-Team: turulomio@yahoo.es\n"
"POT-Creation-Date: 2026-06-07 04:20+0200\n"
"PO-Revision-Date: 2026-06-07 10:15+0200\n"
"Last-Translator: Mariano Muñoz <turulomio@yahoo.es>\n"
"Language-Team: Spanish <turulomio@yahoo.es>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 19.12.3\n"

msgid "There was a problem converting com.sun.star.util.DateTime to datetime."
msgstr "Hubo un problema al convertir com.sun.star.util.DateTime a datetime"
Expand Down Expand Up @@ -335,8 +333,11 @@ msgstr "Esto es un conjunto de símbolos: .,:;?ºª-/()."
msgid "unogenerator_documentation_{0}.ods took {1} in {2}"
msgstr "unogenerator_documentation_{0}.ods tardó {1} en {2}"

msgid "Style name"
msgstr "Nombre de estilo"
msgid "Color name"
msgstr "Nombre del color"

msgid "Hex"
msgstr "Hex"

msgid "Date and time"
msgstr "Fecha y hora"
Expand Down Expand Up @@ -507,3 +508,6 @@ msgstr "La extensión del nombre del fichero debe ser 'ods'."

msgid "Filename extension must be 'xlsx'."
msgstr "La extensión del nombre del fichero debe ser 'xlsx'."

#~ msgid "Style name"
#~ msgstr "Nombre de estilo"
Binary file modified unogenerator/locale/es/LC_MESSAGES/unogenerator.mo
Binary file not shown.
Loading
Loading