diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 0bd4d0df..a2fd49a3 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -5,10 +5,11 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: python-version: ["3.9", "3.10", "3.11"] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v3 diff --git a/README.rst b/README.rst index bf6c6b35..df348434 100644 --- a/README.rst +++ b/README.rst @@ -2,28 +2,46 @@ otoole: OSeMOSYS tools for energy work ================================================== -.. image:: https://coveralls.io/repos/github/OSeMOSYS/otoole/badge.svg?branch=master&kill_cache=1 - :target: https://coveralls.io/github/OSeMOSYS/otoole?branch=master +.. image:: https://joss.theoj.org/papers/e93a191ae795b171beff782a68fdc467/status.svg + :target: https://joss.theoj.org/papers/e93a191ae795b171beff782a68fdc467 + :alt: JOSS status -.. image:: https://readthedocs.org/projects/otoole/badge/?version=latest - :target: https://otoole.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status +.. image:: https://img.shields.io/pypi/v/otoole.svg + :target: https://pypi.org/project/otoole/ + :alt: PyPI .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black + :alt: Code Style -.. image:: https://joss.theoj.org/papers/e93a191ae795b171beff782a68fdc467/status.svg - :target: https://joss.theoj.org/papers/e93a191ae795b171beff782a68fdc467 - :alt: JOSS status +.. image:: https://img.shields.io/badge/python-3.9_|_3.10_|_3.11-blue.svg + :target: https://crate.io/packages/otoole/ + :alt: Python Version -A Python toolkit to support use of OSeMOSYS +.. image:: https://img.shields.io/badge/License-MIT-green.svg + :target: https://opensource.org/licenses/MIT + :alt: License + +| + +.. image:: https://coveralls.io/repos/github/OSeMOSYS/otoole/badge.svg?branch=master&kill_cache=1 + :target: https://coveralls.io/github/OSeMOSYS/otoole?branch=master + :alt: Code Coverage + +.. image:: https://github.com/OSeMOSYS/otoole/actions/workflows/python.yaml/badge.svg?branch=master + :target: https://github.com/OSeMOSYS/otoole/actions/workflows/python.yaml + :alt: GitHub CI + +.. image:: https://readthedocs.org/projects/otoole/badge/?version=latest + :target: https://otoole.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status Description =========== OSeMOSYS tools for energy work, or otoole, is a Python package -which provides a command-line interface for users of OSeMOSYS. The aim of the -package is to provide commonly used pre- and post-processing steps for OSeMOSYS. +to support the users of OSeMOSYS. The aim of the package is to provide commonly +used pre- and post-processing steps for OSeMOSYS. **otoole** aims to support different ways of storing input data and results, including csv files and Excel workbooks, as well as different implementations @@ -53,5 +71,7 @@ Contributing New ideas and bugs `should be submitted `_ to the repository issue tracker. Please do contribute by discussing and developing these -ideas further. To contribute directly to the documentation of code development, please see -the contribution guidelines document. +ideas further. + +To contribute directly to the code and documentation development, please see +the `contribution guidelines `_. diff --git a/docs/examples.rst b/docs/examples.rst index 281a9831..aec99b71 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -215,7 +215,20 @@ Run the following command, where the RES will be saved as the file ``res.png``:: $ otoole viz res excel simplicity.xlsx res.png config.yaml -2. View the RES +.. WARNING:: + If you encounter a ``graphviz`` dependency error, install it on your system + from the graphviz_ website (if on Windows) or via the command:: + + sudo apt install graphviz # if on Ubuntu + brew install graphviz # if on Mac + + To check that ``graphviz`` installed correctly, run ``dot -V`` to check the + version:: + + ~$ dot -V + dot - graphviz version 2.43.0 (0) + +1. View the RES ~~~~~~~~~~~~~~~ Open the newly created file, ``res.png`` and the following image should be displayed @@ -311,8 +324,12 @@ The MathProg datafile describing this model can be found on the ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create a configuration validation ``yaml`` file:: + # on UNIX $ touch validate.yaml + # on Windows + > type nul > validate.yaml + 3. Create ``FUEL`` Codes ~~~~~~~~~~~~~~~~~~~~~~~~ Create the fuel codes and descriptions in the validation configuration file:: @@ -486,3 +503,4 @@ will also flag it as an isolated fuel. This means the fuel is unconnected from t .. _CPLEX: https://www.ibm.com/products/ilog-cplex-optimization-studio/cplex-optimizer .. _Anaconda: https://www.anaconda.com/ .. _Gurobi: https://www.gurobi.com/ +.. _graphviz: https://www.graphviz.org/download/ diff --git a/src/otoole/input.py b/src/otoole/input.py index 0c97c586..b3c979c7 100644 --- a/src/otoole/input.py +++ b/src/otoole/input.py @@ -116,7 +116,6 @@ def convert(self, input_filepath: str, output_filepath: str, **kwargs: Dict): class Strategy(ABC): """ - Arguments --------- user_config : dict, default=None @@ -139,10 +138,20 @@ def _add_dtypes(self, config: Dict): dtypes = {} for column in details["indices"] + ["VALUE"]: if column == "VALUE": - dtypes["VALUE"] = details["dtype"] + dtypes["VALUE"] = ( + details["dtype"] if details["dtype"] != "int" else "int64" + ) else: - dtypes[column] = config[column]["dtype"] + dtypes[column] = ( + config[column]["dtype"] + if config[column]["dtype"] != "int" + else "int64" + ) details["index_dtypes"] = dtypes + elif details["type"] == "set": + details["dtype"] = ( + details["dtype"] if details["dtype"] != "int" else "int64" + ) return config @property @@ -491,8 +500,8 @@ def _check_index_dtypes( except ValueError: # ValueError: invalid literal for int() with base 10: df = df.dropna(axis=0, how="all").reset_index() for index, dtype in config["index_dtypes"].items(): - if dtype == "int": - df[index] = df[index].astype(float).astype(int) + if dtype == "int64": + df[index] = df[index].astype(float).astype("int64") else: df[index] = df[index].astype(dtype) df = df.set_index(config["indices"]) diff --git a/src/otoole/results/result_package.py b/src/otoole/results/result_package.py index 991b3fe2..255f6847 100644 --- a/src/otoole/results/result_package.py +++ b/src/otoole/results/result_package.py @@ -858,7 +858,7 @@ def discount_factor( if regions and years: discount_rate["YEAR"] = [years] discount_factor = discount_rate.explode("YEAR").reset_index(level="REGION") - discount_factor["YEAR"] = discount_factor["YEAR"].astype(int) + discount_factor["YEAR"] = discount_factor["YEAR"].astype("int64") discount_factor["NUM"] = discount_factor["YEAR"] - discount_factor["YEAR"].min() discount_factor["RATE"] = discount_factor["VALUE"] + 1 discount_factor["VALUE"] = ( diff --git a/src/otoole/results/results.py b/src/otoole/results/results.py index dfb7c426..ae45d737 100644 --- a/src/otoole/results/results.py +++ b/src/otoole/results/results.py @@ -347,7 +347,7 @@ def read_model(self, file_path: Union[str, TextIO]) -> pd.DataFrame: df["INDEX"] = df["INDEX"].map(lambda x: x.split("]")[0]) df = ( df[["ID", "NUM", "NAME", "INDEX"]] - .astype({"ID": str, "NUM": int, "NAME": str, "INDEX": str}) + .astype({"ID": str, "NUM": "int64", "NAME": str, "INDEX": str}) .reset_index(drop=True) ) @@ -425,7 +425,7 @@ def read_solution( data = ( data[["ID", "NUM", "STATUS", "PRIM", "DUAL"]] .astype( - {"ID": str, "NUM": int, "STATUS": str, "PRIM": float, "DUAL": float} + {"ID": str, "NUM": "int64", "STATUS": str, "PRIM": float, "DUAL": float} ) .reset_index(drop=True) ) diff --git a/src/otoole/write_strategies.py b/src/otoole/write_strategies.py index 1d10ef5d..d4472f8b 100644 --- a/src/otoole/write_strategies.py +++ b/src/otoole/write_strategies.py @@ -156,7 +156,12 @@ def _write_parameter( df = self._form_parameter(df, default) handle.write("param default {} : {} :=\n".format(default, parameter_name)) df.to_csv( - path_or_buf=handle, sep=" ", header=False, index=True, float_format="%g" + path_or_buf=handle, + sep=" ", + header=False, + index=True, + float_format="%g", + lineterminator="\n", ) handle.write(";\n") @@ -171,7 +176,12 @@ def _write_set(self, df: pd.DataFrame, set_name, handle: TextIO): """ handle.write("set {} :=\n".format(set_name)) df.to_csv( - path_or_buf=handle, sep=" ", header=False, index=False, float_format="%g" + path_or_buf=handle, + sep=" ", + header=False, + index=False, + float_format="%g", + lineterminator="\n", ) handle.write(";\n") diff --git a/tests/test_cli.py b/tests/test_cli.py index 17a8b40d..3e109c03 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -41,15 +41,20 @@ def test_version(self): result = run(["otoole", "--version"], capture_output=True) assert result.stdout.strip().decode() == str(__version__) + def test_help(self): + commands = ["otoole", "-v", "convert", "--help"] + expected = "usage: otoole convert [-h]" + actual = run(commands, capture_output=True) + assert expected in str(actual.stdout) + assert actual.returncode == 0, print(actual.stdout) + temp = mkdtemp() - temp_excel = NamedTemporaryFile(suffix=".xlsx") - temp_datafile = NamedTemporaryFile(suffix=".dat") simplicity = os.path.join("tests", "fixtures", "simplicity.txt") config_path = os.path.join("tests", "fixtures", "config.yaml") test_data = [ - (["otoole", "-v", "convert", "--help"], "usage: otoole convert [-h]"), ( + "excel", [ "otoole", "-v", @@ -57,12 +62,13 @@ def test_version(self): "datafile", "excel", simplicity, - temp_excel.name, + "convert_to_file_path", # replaced with NamedTemporaryFile config_path, ], "", ), ( + "datafile", [ "otoole", "-v", @@ -70,19 +76,34 @@ def test_version(self): "datafile", "datafile", simplicity, - temp_datafile.name, + "convert_to_file_path", # replaced with NamedTemporaryFile config_path, ], "", ), ] - @mark.parametrize("commands,expected", test_data, ids=["help", "excel", "datafile"]) - def test_convert_commands(self, commands, expected): - actual = run(commands, capture_output=True) - assert expected in str(actual.stdout) - print(" ".join(commands)) - assert actual.returncode == 0, print(actual.stdout) + @mark.parametrize( + "convert_to,commands,expected", test_data, ids=["excel", "datafile"] + ) + def test_convert_commands(self, convert_to, commands, expected): + if convert_to == "datafile": + temp = NamedTemporaryFile(suffix=".dat", delete=False, mode="w") + elif convert_to == "excel": + temp = NamedTemporaryFile(suffix=".xlsx", delete=False, mode="w") + else: + raise NotImplementedError + try: + commands_adjusted = [ + x if x != "convert_to_file_path" else temp.name for x in commands + ] + actual = run(commands_adjusted, capture_output=True) + assert expected in str(actual.stdout) + print(" ".join(commands_adjusted)) + assert actual.returncode == 0, print(actual.stdout) + finally: + temp.close() + os.unlink(temp.name) test_errors = [ ( @@ -98,59 +119,68 @@ def test_convert_error(self, commands, expected): def test_convert_datafile_datafile_no_user_config(self): simplicity = os.path.join("tests", "fixtures", "simplicity.txt") - temp_datafile = NamedTemporaryFile(suffix=".dat") - commands = [ - "otoole", - "convert", - "datafile", - "datafile", - simplicity, - temp_datafile.name, - ] - actual = run(commands, capture_output=True) - assert actual.returncode == 2 + temp_datafile = NamedTemporaryFile(suffix=".dat", delete=False, mode="w") + try: + commands = [ + "otoole", + "convert", + "datafile", + "datafile", + simplicity, + temp_datafile.name, + ] + actual = run(commands, capture_output=True) + assert actual.returncode == 2 + finally: + temp_datafile.close() + os.unlink(temp_datafile.name) def test_convert_datafile_datafile_with_user_config(self): simplicity = os.path.join("tests", "fixtures", "simplicity.txt") user_config = os.path.join("tests", "fixtures", "config.yaml") - temp_datafile = NamedTemporaryFile(suffix=".dat") - commands = [ - "otoole", - "-vvv", - "convert", - "datafile", - "datafile", - simplicity, - temp_datafile.name, - user_config, - ] - actual = run(commands, capture_output=True) - assert actual.returncode == 0 + temp_datafile = NamedTemporaryFile(suffix=".dat", delete=False, mode="w") + try: + commands = [ + "otoole", + "-vvv", + "convert", + "datafile", + "datafile", + simplicity, + temp_datafile.name, + user_config, + ] + actual = run(commands, capture_output=True) + assert actual.returncode == 0 + finally: + temp_datafile.close() + os.unlink(temp_datafile.name) def test_convert_datafile_datafile_with_default_flag(self): simplicity = os.path.join("tests", "fixtures", "simplicity.txt") user_config = os.path.join("tests", "fixtures", "config.yaml") - temp_datafile = NamedTemporaryFile(suffix=".dat") - commands = [ - "otoole", - "-vvv", - "convert", - "datafile", - "datafile", - simplicity, - temp_datafile.name, - user_config, - "--write_defaults", - ] - actual = run(commands, capture_output=True) - assert actual.returncode == 0 + temp_datafile = NamedTemporaryFile(suffix=".dat", delete=False, mode="w") + try: + commands = [ + "otoole", + "-vvv", + "convert", + "datafile", + "datafile", + simplicity, + temp_datafile.name, + user_config, + "--write_defaults", + ] + actual = run(commands, capture_output=True) + assert actual.returncode == 0 + finally: + temp_datafile.close() + os.unlink(temp_datafile.name) class TestSetup: - temp = mkdtemp() - temp_config = NamedTemporaryFile(suffix=".yaml") - test_data = [ ( [ @@ -158,27 +188,45 @@ class TestSetup: "-v", "setup", "config", - NamedTemporaryFile(suffix=".yaml").name, + NamedTemporaryFile( + suffix=".yaml" + ).name, # representes a new config file ], "", ), - (["otoole", "-v", "setup", "config", temp_config.name, "--overwrite"], ""), + (["otoole", "-v", "setup", "config", "temp_file", "--overwrite"], ""), ] @mark.parametrize( "commands,expected", test_data, ids=["setup", "setup_with_overwrite"] ) def test_setup_commands(self, commands, expected): - actual = run(commands, capture_output=True) - assert expected in str(actual.stdout) - print(" ".join(commands)) - assert actual.returncode == 0, print(actual.stdout) + temp_yaml = NamedTemporaryFile(suffix=".yaml", delete=False, mode="w+b") + try: + commands_adjusted = [ + x if x != "temp_file" else temp_yaml.name for x in commands + ] + actual = run(commands_adjusted, capture_output=True) + assert expected in str(actual.stdout) + print(" ".join(commands_adjusted)) + assert actual.returncode == 0, print(actual.stdout) + finally: + temp_yaml.close() + os.unlink(temp_yaml.name) test_errors = [ - (["otoole", "-v", "setup", "config", temp_config.name], "OtooleSetupError"), + (["otoole", "-v", "setup", "config", "temp_file"], "OtooleSetupError"), ] @mark.parametrize("commands,expected", test_errors, ids=["setup_fails"]) def test_setup_error(self, commands, expected): - actual = run(commands, capture_output=True) - assert expected in str(actual.stderr) + temp_yaml = NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") + try: + commands_adjusted = [ + x if x != "temp_file" else temp_yaml.name for x in commands + ] + actual = run(commands_adjusted, capture_output=True) + assert expected in str(actual.stderr) + finally: + temp_yaml.close() + os.unlink(temp_yaml.name) diff --git a/tests/test_convert.py b/tests/test_convert.py index 8e94bccd..e4c99046 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -59,21 +59,32 @@ class TestWrite: def test_write_datafile(self): """Test writing data to a file""" data = {"REGION": pd.DataFrame({"VALUE": ["BB"]})} - temp = NamedTemporaryFile() - assert write( - os.path.join("tests", "fixtures", "config.yaml"), - "datafile", - temp.name, - data, - ) + temp = NamedTemporaryFile(delete=False, mode="w") + try: + assert write( + os.path.join("tests", "fixtures", "config.yaml"), + "datafile", + temp.name, + data, + ) + finally: + temp.close() + os.unlink(temp.name) def test_write_excel(self): """Test writing data to an Excel file""" data = {"REGION": pd.DataFrame({"VALUE": ["BB"]})} - temp = NamedTemporaryFile(suffix=".xlsx") - assert write( - os.path.join("tests", "fixtures", "config.yaml"), "excel", temp.name, data - ) + temp = NamedTemporaryFile(suffix=".xlsx", delete=False, mode="w") + try: + assert write( + os.path.join("tests", "fixtures", "config.yaml"), + "excel", + temp.name, + data, + ) + finally: + temp.close() + os.unlink(temp.name) def test_write_csv(self): """Test writing data to a CSV file""" @@ -92,21 +103,22 @@ class TestConvert: def test_convert_excel_to_datafile(self): """Test converting from Excel to datafile""" - user_config = os.path.join("tests", "fixtures", "config.yaml") - tmpfile = NamedTemporaryFile() from_path = os.path.join("tests", "fixtures", "combined_inputs.xlsx") - - convert(user_config, "excel", "datafile", from_path, tmpfile.name) - - tmpfile.seek(0) - actual = tmpfile.readlines() - tmpfile.close() - - assert actual[-1] == b"end;\n" - assert actual[0] == b"# Model file written by *otoole*\n" - assert actual[2] == b"09_ROK d_bld_2_coal_products 2017 20.8921\n" - assert actual[8996] == b"param default 1 : DepreciationMethod :=\n" + tmpfile = NamedTemporaryFile(delete=False, mode="w+b") + + try: + convert(user_config, "excel", "datafile", from_path, tmpfile.name) + tmpfile.seek(0) + actual = tmpfile.readlines() + + assert actual[-1] == b"end;\n" + assert actual[0] == b"# Model file written by *otoole*\n" + assert actual[2] == b"09_ROK d_bld_2_coal_products 2017 20.8921\n" + assert actual[8996] == b"param default 1 : DepreciationMethod :=\n" + finally: + tmpfile.close() + os.unlink(tmpfile.name) def test_convert_excel_to_csv(self): """Test converting from Excel to CSV""" diff --git a/tests/test_read_strategies.py b/tests/test_read_strategies.py index a7a20d10..574fcee8 100644 --- a/tests/test_read_strategies.py +++ b/tests/test_read_strategies.py @@ -108,7 +108,7 @@ def test_solution_to_dataframe(self, user_config): ], columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"], ) - .astype({"REGION": str, "TECHNOLOGY": str, "YEAR": int, "VALUE": float}) + .astype({"REGION": str, "TECHNOLOGY": str, "YEAR": "int64", "VALUE": float}) .set_index(["REGION", "TECHNOLOGY", "YEAR"]) ) @@ -135,8 +135,8 @@ def test_solution_to_dataframe(self, user_config): "REGION": str, "TIMESLICE": str, "TECHNOLOGY": str, - "MODE_OF_OPERATION": int, - "YEAR": int, + "MODE_OF_OPERATION": "int64", + "YEAR": "int64", "VALUE": float, } ) @@ -202,7 +202,7 @@ def test_solution_to_dataframe(self, user_config): ], columns=["REGION", "YEAR", "VALUE"], ) - .astype({"YEAR": int, "VALUE": float}) + .astype({"YEAR": "int64", "VALUE": float}) .set_index(["REGION", "YEAR"]) ) @@ -225,7 +225,7 @@ def test_solution_to_dataframe(self, user_config): "VALUE", ], ) - .astype({"YEAR": int, "VALUE": float, "MODE_OF_OPERATION": int}) + .astype({"YEAR": "int64", "VALUE": float, "MODE_OF_OPERATION": "int64"}) .set_index( ["REGION", "TIMESLICE", "TECHNOLOGY", "MODE_OF_OPERATION", "YEAR"] ) @@ -623,7 +623,7 @@ def test_read_model(self, user_config): ["j", 1028, "RateOfActivity", "SIMPLICITY,IN,BACKSTOP1,1,2014"], ], columns=["ID", "NUM", "NAME", "INDEX"], - ) + ).astype({"ID": str, "NUM": "int64", "NAME": str, "INDEX": str}) pd.testing.assert_frame_equal(actual, expected) @@ -700,7 +700,7 @@ def test_index_dtypes_available(self, user_config): assert actual == { "REGION": "str", "FUEL": "str", - "YEAR": "int", + "YEAR": "int64", "VALUE": "float", } @@ -726,7 +726,7 @@ def test_remove_empty_lines(self, user_config): ], columns=["REGION", "FUEL", "YEAR", "VALUE"], ) - .astype({"REGION": str, "FUEL": str, "YEAR": int, "VALUE": float}) + .astype({"REGION": str, "FUEL": str, "YEAR": "int64", "VALUE": float}) .set_index(["REGION", "FUEL", "YEAR"]) } @@ -757,7 +757,7 @@ def test_change_types(self, user_config): ], columns=["REGION", "FUEL", "YEAR", "VALUE"], ) - .astype({"REGION": str, "FUEL": str, "YEAR": int, "VALUE": float}) + .astype({"REGION": str, "FUEL": str, "YEAR": "int64", "VALUE": float}) .set_index(["REGION", "FUEL", "YEAR"]) } @@ -834,7 +834,7 @@ def test_read_config(self, user_config): "FUEL": "str", "REGION": "str", "VALUE": "float", - "YEAR": "int", + "YEAR": "int64", }, } assert actual["AccumulatedAnnualDemand"] == expected diff --git a/tests/test_utils.py b/tests/test_utils.py index 8fac9aa3..1a50ccf8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,4 @@ +import os from tempfile import NamedTemporaryFile import pandas as pd @@ -77,16 +78,18 @@ def test_create_name_mappings_reversed(self, user_config): def test_excel_name_length_error(user_config_simple, request): user_config = request.getfixturevalue(user_config_simple) write_excel = WriteExcel(user_config=user_config) - temp_excel = NamedTemporaryFile(suffix=".xlsx") - handle = pd.ExcelWriter(temp_excel.name) - - with pytest.raises(OtooleExcelNameLengthError): - write_excel._write_parameter( - df=pd.DataFrame(), - parameter_name="ParameterNameLongerThanThirtyOneChars", - handle=pd.ExcelWriter(handle), - default=0, - ) + temp_excel = NamedTemporaryFile(suffix=".xlsx", delete=False, mode="r") + try: + with pytest.raises(OtooleExcelNameLengthError): + write_excel._write_parameter( + df=pd.DataFrame(), + parameter_name="ParameterNameLongerThanThirtyOneChars", + handle=pd.ExcelWriter(temp_excel.name), + default=0, + ) + finally: + temp_excel.close() + os.unlink(temp_excel.name) class TestYamlUniqueKeyReader: diff --git a/tests/test_write_strategies.py b/tests/test_write_strategies.py index af4d8b8e..18cf64ae 100644 --- a/tests/test_write_strategies.py +++ b/tests/test_write_strategies.py @@ -1,4 +1,5 @@ import io +import os from tempfile import NamedTemporaryFile import pandas as pd @@ -114,15 +115,20 @@ def test_form_no_pivot(self, user_config): def test_write_out_empty_dataframe(self, user_config): - temp_excel = NamedTemporaryFile(suffix=".xlsx") - handle = pd.ExcelWriter(temp_excel.name) - convert = WriteExcel(user_config) + temp_excel = NamedTemporaryFile(suffix=".xlsx", delete=False, mode="w") + try: + handle = pd.ExcelWriter(temp_excel.name) + convert = WriteExcel(user_config) - df = pd.DataFrame( - data=None, columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"] - ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) + df = pd.DataFrame( + data=None, columns=["REGION", "TECHNOLOGY", "YEAR", "VALUE"] + ).set_index(["REGION", "TECHNOLOGY", "YEAR"]) - convert._write_parameter(df, "AvailabilityFactor", handle, default=0) + convert._write_parameter(df, "AvailabilityFactor", handle, default=0) + finally: + handle.close() + temp_excel.close() + os.unlink(temp_excel.name) class TestWriteDatafile: