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
63 changes: 11 additions & 52 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
robotframework: ["7.0", "7.4.2"]
steps:
- uses: actions/checkout@v4

Expand All @@ -53,64 +54,22 @@ jobs:
run: pip install hatch

- name: Install dependencies
run: hatch env create dev

- name: Build wheel with Hatch
run: hatch build

- name: Install built wheel
run: pip install dist/*.whl

- name: Test import
run: python -c "import Tables"
run: hatch env create test.py${{ matrix.python-version }}-${{ matrix.robotframework }}

run-acceptance-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Show installed Python version
run: hatch run test.py${{ matrix.python-version }}-${{ matrix.robotframework }}:python --version

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Hatch
run: pip install hatch

- name: Install dependencies
run: hatch env create dev
- name: Show installed Robot Framework version
run: hatch run test.py${{ matrix.python-version }}-${{ matrix.robotframework }}:robot --version || test $? -eq 251

- name: Build wheel with Hatch
run: hatch build

- name: Run robot framework acceptance-tests
run: hatch run dev:atest

run-unit-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Hatch
run: pip install hatch

- name: Install dependencies
run: hatch env create dev

- name: Build wheel with Hatch
run: hatch build
- name: Run unit tests
run: hatch run test.py${{ matrix.python-version }}-${{ matrix.robotframework }}:utest

- name: Run unit tests with pytest
run: hatch run dev:utest
- name: Run acceptance tests
run: hatch run test.py${{ matrix.python-version }}-${{ matrix.robotframework }}:atest

generate-libdoc-docs:
runs-on: ubuntu-latest
Expand Down
18 changes: 18 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ hatch run dev:atest
hatch run dev:utest
```

### Execute Tests Against the Version Matrix

Install Python 3.10 through 3.14 locally and make sure Hatch can find these interpreters. The test matrix covers Python 3.10-3.14 and Robot Framework 7.0 and 7.4.2.

Run all unit and acceptance tests against every matrix combination:

```shell
hatch run test:all
```

To test one combination only, for example Python 3.14 with Robot Framework 7.4.2:

```shell
hatch run test.py3.14-7.4.2:all
```

Use `hatch env show` to list the generated matrix environments.

### Generate Docs via libdoc

```shell
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ ${result} = BuiltIn.Evaluate "index" not in "${content}"
BuiltIn.Should Be True ${result}
```

### Missing values

Missing values are returned using pandas' default values by default. To return parsed missing values as Python `None` in list, dictionary, cell, row, and column results, enable the setting. This includes pandas missing-value types such as `NaN`, `pd.NA`, and `NaT`, as well as values recognized as missing by the active parser, such as `N/A` and `NULL`:

```
Tables.Configure Missing As None True
${content} = Tables.Read Table ${CURDIR}${/}testdata${/}example_01.csv
```

DataFrame results and the library's internal DataFrames are not changed by this setting.

### File Format - Parquet
```
${content} = Tables.Read Table ${CURDIR}${/}testdata${/}example_05.parquet
Expand Down
20 changes: 20 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"overrides",
Expand Down Expand Up @@ -70,6 +72,22 @@ extra-dependencies = [
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/Tables tests}"

[tool.hatch.envs.test]
dependencies = [
"pytest",
"pytest-cov",
"robotframework=={matrix:robotframework}",
]

[tool.hatch.envs.test.scripts]
atest = "robot tests/atest/"
utest = "pytest --cov=Tables --cov-report=term-missing --cov-branch --cov-fail-under=95"
all = ["utest", "atest"]

[[tool.hatch.envs.test.matrix]]
python = ["3.10", "3.11", "3.12", "3.13", "3.14"]
robotframework = ["7.0", "7.4.2"]

[tool.coverage.run]
source_pkgs = ["Tables", "tests"]
branch = true
Expand Down Expand Up @@ -103,6 +121,8 @@ docs = "libdoc src/Tables results/keyword_docs.html"

[tool.mypy]
python_version = "3.10"
exclude = '(^|/)\.venv(/|$)'
follow_imports = "skip"
ignore_missing_imports = true
no_implicit_optional = true
strict_optional = true
Expand Down
18 changes: 12 additions & 6 deletions src/Tables/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def __init__( # noqa: PLR0913
line_terminator: LineTerminator = LineTerminator.LF,
quoting: Quoting = Quoting.MINIMAL,
quoting_character: QuotingCharacter = QuotingCharacter['"'],
missing_as_none: bool = False,
):
"""
``TableLibrary`` can be controlled by the following arguments:
Expand All @@ -184,6 +185,7 @@ def __init__( # noqa: PLR0913
| ``line_terminator`` | Define the required line terminator for your table files. Default is ``False`` |
| ``quoting`` | Define which values should be surrounded with quotes, please check the CSV quoting for more details. Default is ``MINIMAL`` |
| ``quoting_character`` | Define quoting character to use for writing table files. Default is ``\"`` |
| ``missing_as_none`` | Return parsed missing values as Python ``None`` instead of pandas missing values. Default is ``False`` |
"""

# required variables for SettingsScope mechanism
Expand All @@ -202,6 +204,7 @@ def __init__( # noqa: PLR0913
)
self.scope_stack["quoting"] = SettingsStack(quoting, self)
self.scope_stack["quoting_character"] = SettingsStack(quoting_character, self)
self.scope_stack["missing_as_none"] = SettingsStack(missing_as_none, self)
self.scope_stack["enable_streaming"] = SettingsStack(False, self)

self.file_access = FileAccess(self)
Expand All @@ -222,12 +225,14 @@ def __init__( # noqa: PLR0913
super().__init__(libraries)

def _start_suite(self, _name: running.TestSuite, attrs: result.TestSuite):
self.suite_ids[attrs.id] = None
self._add_to_scope_stack(attrs.id, Scope.Suite)
suite_id = attrs.id
self.suite_ids[suite_id] = None
self._add_to_scope_stack(suite_id, Scope.Suite)

def _start_test(self, _name: running.TestCase, attrs: result.TestCase):
self.current_test_id = attrs.id
self._add_to_scope_stack(attrs.id, Scope.Test)
test_id = attrs.id
self.current_test_id = test_id
self._add_to_scope_stack(test_id, Scope.Test)
self.is_test_case_running = True

def _end_test(self, _name: running.TestCase, attrs: result.TestCase):
Expand All @@ -236,8 +241,9 @@ def _end_test(self, _name: running.TestCase, attrs: result.TestCase):
self.is_test_case_running = False

def _end_suite(self, _name: running.TestSuite, attrs: result.TestSuite):
self._remove_from_scope_stack(attrs.id)
self.suite_ids.pop(attrs.id, None)
suite_id = attrs.id
self._remove_from_scope_stack(suite_id)
self.suite_ids.pop(suite_id, None)

def _add_to_scope_stack(self, scope_id: str, scope: Scope):
for stack in self.scope_stack.values():
Expand Down
13 changes: 13 additions & 0 deletions src/Tables/general/library_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,16 @@ def enable_streaming_stack(self) -> SettingsStack:
@enable_streaming_stack.setter
def enable_streaming_stack(self, stack: SettingsStack):
self.library.scope_stack["enable_streaming"] = stack

# missing values
@property
def missing_as_none(self) -> bool:
return self.library.scope_stack["missing_as_none"].get()

@property
def missing_as_none_stack(self) -> SettingsStack:
return self.library.scope_stack["missing_as_none"]

@missing_as_none_stack.setter
def missing_as_none_stack(self, stack: SettingsStack):
self.library.scope_stack["missing_as_none"] = stack
25 changes: 25 additions & 0 deletions src/Tables/keywords/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ConfigCtx:
file_encoding: FileEncoding
ignore_header: bool
enable_streaming: bool
missing_as_none: bool


class Configuration(LibraryAttributes):
Expand All @@ -45,6 +46,7 @@ def get_library_configuration(self) -> dict:
file_encoding=self.file_encoding,
ignore_header=self.ignore_header,
enable_streaming=self.enable_streaming,
missing_as_none=self.missing_as_none,
)
)

Expand Down Expand Up @@ -199,3 +201,26 @@ def configure_streaming(self, enable_streaming: bool, scope: Scope = Scope.Suite
self.enable_streaming_stack.set(enable_streaming, scope)
logger.debug(f"'file_type': old value: {old_config} - new value: {enable_streaming}")
return old_config

@keyword(tags=["Configuration"])
def configure_missing_as_none(self, missing_as_none: bool, scope: Scope = Scope.Suite):
"""
Configure whether parsed missing values are returned as Python ``None``.

When enabled, pandas missing values such as ``NaN``, ``pd.NA`` and ``NaT``
are converted to ``None`` in list, dictionary, cell, row and column results.
Values recognized as missing by the active parser, such as ``N/A`` and ``NULL``,
are included. DataFrame results and the library's internal DataFrames are not changed.

| =`Arguments`= | =`Description`= |
| ``missing_as_none`` | Set to ``True`` to return missing values as Python ``None``. Default: ``False`` |
| ``scope`` | Scope in which the setting applies. |

== Example ==
| Configure Missing As None True
| Configure Missing As None False
"""
old_config = self.missing_as_none
self.missing_as_none_stack.set(missing_as_none, scope)
logger.debug(f"'missing_as_none': old value: {old_config} - new value: {missing_as_none}")
return old_config
25 changes: 21 additions & 4 deletions src/Tables/keywords/getter.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@ def read_table(
table_df = table_df.iloc[1:]
data = data[1:]

data = self.file_reader.convert_missing_values(data)

if return_type == TableFormat["List of dicts"]:
df_for_dicts = table_df

if self.file_type != FileType.Parquet and not self.ignore_header and not table_df.empty:
header = [str(x) for x in df_for_dicts.iloc[0].tolist()]
df_for_dicts = df_for_dicts.iloc[1:].copy()
df_for_dicts.columns = header
return cast(list[dict[str, Any]], df_for_dicts.to_dict(orient="records"))
return self.file_reader.convert_missing_values(
cast(list[dict[str, Any]], df_for_dicts.to_dict(orient="records"))
)
return data

@keyword(tags=["Getter"])
Expand Down Expand Up @@ -196,6 +200,9 @@ def get_table(self, return_type: TableFormat = TableFormat["List of lists"]) ->
| =`Arguments`= | =`Description`= |
| ``return_type`` | Choose what type of table format it should return. Default: list of lists|

If ``Configure Missing As None`` is enabled, missing values are returned as
Python ``None`` for list and dictionary results. DataFrame results are unchanged.

== Return Value ==
Return table in form as either list of lists, list of dicts or dataframe.

Expand Down Expand Up @@ -229,6 +236,9 @@ def get_table_cell(
| ``assertion_expected`` | See ``robotframework-assertion-engine`` for more details |
| ``message`` | Custom error message for failed assertion |

If ``Configure Missing As None`` is enabled, a missing cell is returned as
Python ``None`` and assertions are evaluated against the converted value.

== Return Value ==
Keyword returns the value of the given cell.
In case of a failed assertion, the keyword will just fail without returning anything.
Expand All @@ -245,6 +255,7 @@ def get_table_cell(
column = self.file_reader.cast_column_type(column)

cell = table_df.loc[row, column] if isinstance(column, str) else table_df.iloc[row, column]
cell = self.file_reader.convert_missing_values(cell)

if assertion_expected:
if assertion_operator not in NumericalOperators:
Expand Down Expand Up @@ -272,6 +283,9 @@ def get_table_column(
| ``assertion_expected`` | See ``robotframework-assertion-engine`` for more details |
| ``message`` | Custom error message for failed assertion |

If ``Configure Missing As None`` is enabled, missing column values are returned
as Python ``None`` and assertions are evaluated against the converted values.

== Return Value ==
Returns column values as a list.

Expand All @@ -291,7 +305,7 @@ def get_table_column(
column = self.file_reader.cast_column_type(column)

column_df = table_df.loc[:, column] if isinstance(column, str) else table_df.iloc[:, column]
column_list = cast(list[Any], column_df.to_list())
column_list = self.file_reader.convert_missing_values(cast(list[Any], column_df.to_list()))

if assertion_expected:
if assertion_operator not in valid_assertions:
Expand All @@ -318,6 +332,9 @@ def get_table_row(
| ``assertion_expected`` | See ``robotframework-assertion-engine`` for more details |
| ``message`` | Custom error message for failed assertion |

If ``Configure Missing As None`` is enabled, missing row values are returned as
Python ``None`` and assertions are evaluated against the converted values.

== Return Value ==
Returns row values as a list.

Expand All @@ -335,7 +352,7 @@ def get_table_row(
current_df = self.file_reader.file_sync.table_storage[self.file_reader.file_sync.current_file].data
table_df = self.file_reader.validate_table_to_dataframe(data=current_df, row=row)

row_list = cast(list[Any], table_df.iloc[row].to_list())
row_list = self.file_reader.convert_missing_values(cast(list[Any], table_df.iloc[row].to_list()))

if assertion_expected:
if assertion_operator not in valid_assertions:
Expand All @@ -347,7 +364,7 @@ def get_table_row(
return row_list

@keyword(tags=["Getter"])
def count_table( # noqa: PLR0913
def count_table( # noqa PLR0913
self,
path: Path | str,
axis: Axis,
Expand Down
Loading
Loading