diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66e32bd..e51b0a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,13 +14,13 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: 'pip' # caching pip dependencies + - run: python -m pip install -r requirements.txt - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions + run: python -m pip install tox tox-gh-actions - name: Test with tox run: tox - name: Coveralls diff --git a/tests/test_crossrefs.py b/tests/test_crossrefs.py index c54ce3a..320772a 100644 --- a/tests/test_crossrefs.py +++ b/tests/test_crossrefs.py @@ -1,6 +1,6 @@ -from . import testing +from tests import testing class CrossSheetTest(testing.FunctionalTestCase): diff --git a/tests/test_evaluator.py b/tests/test_evaluator.py index 0dd901c..22eae6a 100644 --- a/tests/test_evaluator.py +++ b/tests/test_evaluator.py @@ -1,7 +1,7 @@ import unittest from xlcalculator import evaluator, model -from . import testing +from tests import testing class TestEvaluator(unittest.TestCase): diff --git a/tests/test_model.py b/tests/test_model.py index 7f59387..57d183a 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -9,7 +9,7 @@ from xlcalculator.tokenizer import f_token from xlcalculator import Evaluator -from . import testing +from tests import testing class ModelTest(testing.XlCalculatorTestCase): diff --git a/tests/test_reader.py b/tests/test_reader.py index 0ff80f7..cde4427 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -2,7 +2,7 @@ import unittest from xlcalculator import reader, xltypes, tokenizer -from . import testing +from tests import testing class ReaderTest(unittest.TestCase): @@ -47,8 +47,5 @@ def test_read_defined_names(self): archive.read() defined_names = archive.read_defined_names() - its_a_blank = ( - set(sorted(defined_names.keys())) - - set(sorted(self.defined_names.keys())) - ) + its_a_blank = (set(sorted(defined_names.keys())) - set(sorted(self.defined_names.keys()))) self.assertEqual(list(its_a_blank), ['Its_a_blank']) diff --git a/tests/test_tokenizer.py b/tests/test_tokenizer.py index 3687772..801ab7b 100644 --- a/tests/test_tokenizer.py +++ b/tests/test_tokenizer.py @@ -1,6 +1,6 @@ from xlcalculator.tokenizer import f_token, ExcelParser -from . import testing +from tests import testing class ExcelParserTest(testing.XlCalculatorTestCase): diff --git a/tests/test_xltypes.py b/tests/test_xltypes.py index 1d0e22f..445540e 100644 --- a/tests/test_xltypes.py +++ b/tests/test_xltypes.py @@ -3,7 +3,7 @@ from xlcalculator import xltypes from xlcalculator.tokenizer import f_token -from . import testing +from tests import testing class XLFormulaTest(testing.XlCalculatorTestCase): diff --git a/tests/testing.py b/tests/testing.py index 3828d88..150c54f 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -109,3 +109,17 @@ def setUp(self): self.model = compiler.read_and_parse_archive( get_resource(self.filename)) self.evaluator = evaluator.Evaluator(self.model) + + +if __name__ == "__main__": + from pathlib import Path + p_dir = Path(__file__).parent + # Discover all tests in the current directory + test_loader = unittest.defaultTestLoader + test_suite = test_loader.discover(start_dir=p_dir, pattern="*.py") + + # Create a test runner + test_runner = unittest.TextTestRunner() + + # Run the tests + test_runner.run(test_suite) diff --git a/tests/xlfunctions_vs_excel/abs_test.py b/tests/xlfunctions_vs_excel/abs_test.py index 461a333..0beb374 100644 --- a/tests/xlfunctions_vs_excel/abs_test.py +++ b/tests/xlfunctions_vs_excel/abs_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ABSTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/acos_test.py b/tests/xlfunctions_vs_excel/acos_test.py index be98f23..81c1f3e 100644 --- a/tests/xlfunctions_vs_excel/acos_test.py +++ b/tests/xlfunctions_vs_excel/acos_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ACOSTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/asin_test.py b/tests/xlfunctions_vs_excel/asin_test.py index 521dba8..7829b53 100644 --- a/tests/xlfunctions_vs_excel/asin_test.py +++ b/tests/xlfunctions_vs_excel/asin_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ASINTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/atan2_test.py b/tests/xlfunctions_vs_excel/atan2_test.py index e85dcc3..6d69abf 100644 --- a/tests/xlfunctions_vs_excel/atan2_test.py +++ b/tests/xlfunctions_vs_excel/atan2_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ATAN2Test(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/atan_test.py b/tests/xlfunctions_vs_excel/atan_test.py index 8c37324..0659445 100644 --- a/tests/xlfunctions_vs_excel/atan_test.py +++ b/tests/xlfunctions_vs_excel/atan_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ATANTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/average_test.py b/tests/xlfunctions_vs_excel/average_test.py index f91c817..9661289 100644 --- a/tests/xlfunctions_vs_excel/average_test.py +++ b/tests/xlfunctions_vs_excel/average_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class AverageTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/ceiling_test.py b/tests/xlfunctions_vs_excel/ceiling_test.py index 8410460..8dccadc 100644 --- a/tests/xlfunctions_vs_excel/ceiling_test.py +++ b/tests/xlfunctions_vs_excel/ceiling_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CEILINGTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/choose_test.py b/tests/xlfunctions_vs_excel/choose_test.py index 8b0198c..bf84315 100644 --- a/tests/xlfunctions_vs_excel/choose_test.py +++ b/tests/xlfunctions_vs_excel/choose_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ChooseTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/concat_test.py b/tests/xlfunctions_vs_excel/concat_test.py index a8ac723..0643f75 100644 --- a/tests/xlfunctions_vs_excel/concat_test.py +++ b/tests/xlfunctions_vs_excel/concat_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ConcatTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/concatenate_test.py b/tests/xlfunctions_vs_excel/concatenate_test.py index cbd3eb8..732ad62 100644 --- a/tests/xlfunctions_vs_excel/concatenate_test.py +++ b/tests/xlfunctions_vs_excel/concatenate_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ConcatenateTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/cos_test.py b/tests/xlfunctions_vs_excel/cos_test.py index 004a819..49b7636 100644 --- a/tests/xlfunctions_vs_excel/cos_test.py +++ b/tests/xlfunctions_vs_excel/cos_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class COSTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/cosh_test.py b/tests/xlfunctions_vs_excel/cosh_test.py index 0cc9d29..fa24787 100644 --- a/tests/xlfunctions_vs_excel/cosh_test.py +++ b/tests/xlfunctions_vs_excel/cosh_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class COSHTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/count_test.py b/tests/xlfunctions_vs_excel/count_test.py index c76c73e..53cf4c0 100644 --- a/tests/xlfunctions_vs_excel/count_test.py +++ b/tests/xlfunctions_vs_excel/count_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CountTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/counta_test.py b/tests/xlfunctions_vs_excel/counta_test.py index 178434a..b95bfb4 100644 --- a/tests/xlfunctions_vs_excel/counta_test.py +++ b/tests/xlfunctions_vs_excel/counta_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CountaTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/countif_test.py b/tests/xlfunctions_vs_excel/countif_test.py index 10b6da2..1e49b96 100644 --- a/tests/xlfunctions_vs_excel/countif_test.py +++ b/tests/xlfunctions_vs_excel/countif_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CountIfTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/countifs_test.py b/tests/xlfunctions_vs_excel/countifs_test.py index ec786f0..b952627 100644 --- a/tests/xlfunctions_vs_excel/countifs_test.py +++ b/tests/xlfunctions_vs_excel/countifs_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CountIfsTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/date_test.py b/tests/xlfunctions_vs_excel/date_test.py index b799ee2..c24a959 100644 --- a/tests/xlfunctions_vs_excel/date_test.py +++ b/tests/xlfunctions_vs_excel/date_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class DateTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/datedif_test.py b/tests/xlfunctions_vs_excel/datedif_test.py index 08fcb04..ff1dc14 100644 --- a/tests/xlfunctions_vs_excel/datedif_test.py +++ b/tests/xlfunctions_vs_excel/datedif_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class DatedifTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/day_test.py b/tests/xlfunctions_vs_excel/day_test.py index a70e84a..a8d3347 100644 --- a/tests/xlfunctions_vs_excel/day_test.py +++ b/tests/xlfunctions_vs_excel/day_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class DayTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/days_test.py b/tests/xlfunctions_vs_excel/days_test.py index e125236..407205f 100644 --- a/tests/xlfunctions_vs_excel/days_test.py +++ b/tests/xlfunctions_vs_excel/days_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class DaysTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/double_minus_test.py b/tests/xlfunctions_vs_excel/double_minus_test.py index f7b13b1..984d312 100644 --- a/tests/xlfunctions_vs_excel/double_minus_test.py +++ b/tests/xlfunctions_vs_excel/double_minus_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class DoubleMinusTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/edate_test.py b/tests/xlfunctions_vs_excel/edate_test.py index 47feb5f..985dd87 100644 --- a/tests/xlfunctions_vs_excel/edate_test.py +++ b/tests/xlfunctions_vs_excel/edate_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class EDateTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/engineering_test.py b/tests/xlfunctions_vs_excel/engineering_test.py index e0d77e4..7dec812 100644 --- a/tests/xlfunctions_vs_excel/engineering_test.py +++ b/tests/xlfunctions_vs_excel/engineering_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class SumTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/eomonth_test.py b/tests/xlfunctions_vs_excel/eomonth_test.py index 08cb92d..2f91cfa 100644 --- a/tests/xlfunctions_vs_excel/eomonth_test.py +++ b/tests/xlfunctions_vs_excel/eomonth_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class EDateTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/exact_test.py b/tests/xlfunctions_vs_excel/exact_test.py index 870d399..8481d21 100644 --- a/tests/xlfunctions_vs_excel/exact_test.py +++ b/tests/xlfunctions_vs_excel/exact_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ExactTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/find_test.py b/tests/xlfunctions_vs_excel/find_test.py index a3d396c..b77505d 100644 --- a/tests/xlfunctions_vs_excel/find_test.py +++ b/tests/xlfunctions_vs_excel/find_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class FindTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/if_test.py b/tests/xlfunctions_vs_excel/if_test.py index e9dc62c..de6dbb2 100644 --- a/tests/xlfunctions_vs_excel/if_test.py +++ b/tests/xlfunctions_vs_excel/if_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CountIfTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/information_test.py b/tests/xlfunctions_vs_excel/information_test.py index 4e4b062..cbc9ad2 100644 --- a/tests/xlfunctions_vs_excel/information_test.py +++ b/tests/xlfunctions_vs_excel/information_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class InformationTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/int_test.py b/tests/xlfunctions_vs_excel/int_test.py index 418e780..1a2af2f 100644 --- a/tests/xlfunctions_vs_excel/int_test.py +++ b/tests/xlfunctions_vs_excel/int_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class IntTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/irr_test.py b/tests/xlfunctions_vs_excel/irr_test.py index 19b7ec9..059fc7a 100644 --- a/tests/xlfunctions_vs_excel/irr_test.py +++ b/tests/xlfunctions_vs_excel/irr_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class IRRTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/len_test.py b/tests/xlfunctions_vs_excel/len_test.py index ee1dfbd..37beeb3 100644 --- a/tests/xlfunctions_vs_excel/len_test.py +++ b/tests/xlfunctions_vs_excel/len_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class LenTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/ln_test.py b/tests/xlfunctions_vs_excel/ln_test.py index 26aeff5..1d3e1ad 100644 --- a/tests/xlfunctions_vs_excel/ln_test.py +++ b/tests/xlfunctions_vs_excel/ln_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class LnTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/match_test.py b/tests/xlfunctions_vs_excel/match_test.py index 6829b99..730f413 100644 --- a/tests/xlfunctions_vs_excel/match_test.py +++ b/tests/xlfunctions_vs_excel/match_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class MatchTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/max_test.py b/tests/xlfunctions_vs_excel/max_test.py index 9a8c422..2025cf0 100644 --- a/tests/xlfunctions_vs_excel/max_test.py +++ b/tests/xlfunctions_vs_excel/max_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class MaxTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/mid_test.py b/tests/xlfunctions_vs_excel/mid_test.py index f66c3ea..750e63b 100644 --- a/tests/xlfunctions_vs_excel/mid_test.py +++ b/tests/xlfunctions_vs_excel/mid_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class MidTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/min_test.py b/tests/xlfunctions_vs_excel/min_test.py index 992da00..995113c 100644 --- a/tests/xlfunctions_vs_excel/min_test.py +++ b/tests/xlfunctions_vs_excel/min_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class MinTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/mod_test.py b/tests/xlfunctions_vs_excel/mod_test.py index 0352c34..4f2dbce 100644 --- a/tests/xlfunctions_vs_excel/mod_test.py +++ b/tests/xlfunctions_vs_excel/mod_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class ModTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/month_test.py b/tests/xlfunctions_vs_excel/month_test.py index 643e108..eb044fc 100644 --- a/tests/xlfunctions_vs_excel/month_test.py +++ b/tests/xlfunctions_vs_excel/month_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class MonthTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/not_test.py b/tests/xlfunctions_vs_excel/not_test.py index 81b0de4..cd65ec6 100644 --- a/tests/xlfunctions_vs_excel/not_test.py +++ b/tests/xlfunctions_vs_excel/not_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class CountIfTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/npv_test.py b/tests/xlfunctions_vs_excel/npv_test.py index a586258..6d334df 100644 --- a/tests/xlfunctions_vs_excel/npv_test.py +++ b/tests/xlfunctions_vs_excel/npv_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class NPVTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/pmt_test.py b/tests/xlfunctions_vs_excel/pmt_test.py index cc666d3..fb7f818 100644 --- a/tests/xlfunctions_vs_excel/pmt_test.py +++ b/tests/xlfunctions_vs_excel/pmt_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class PMTTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/power_test.py b/tests/xlfunctions_vs_excel/power_test.py index 0a4b4e6..3cdb91e 100644 --- a/tests/xlfunctions_vs_excel/power_test.py +++ b/tests/xlfunctions_vs_excel/power_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class PowerTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/right_test.py b/tests/xlfunctions_vs_excel/right_test.py index e25a482..5fdbb23 100644 --- a/tests/xlfunctions_vs_excel/right_test.py +++ b/tests/xlfunctions_vs_excel/right_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class RightTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/round_test.py b/tests/xlfunctions_vs_excel/round_test.py index d42061e..c8ea6e3 100644 --- a/tests/xlfunctions_vs_excel/round_test.py +++ b/tests/xlfunctions_vs_excel/round_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class RoundTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/rounddown_test.py b/tests/xlfunctions_vs_excel/rounddown_test.py index c9df00b..4eb0670 100644 --- a/tests/xlfunctions_vs_excel/rounddown_test.py +++ b/tests/xlfunctions_vs_excel/rounddown_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class RounddownTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/roundup_test.py b/tests/xlfunctions_vs_excel/roundup_test.py index 7c1de12..148832d 100644 --- a/tests/xlfunctions_vs_excel/roundup_test.py +++ b/tests/xlfunctions_vs_excel/roundup_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class RoundupTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/sln_test.py b/tests/xlfunctions_vs_excel/sln_test.py index d77c116..112c867 100644 --- a/tests/xlfunctions_vs_excel/sln_test.py +++ b/tests/xlfunctions_vs_excel/sln_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class SLNTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/sqrt_test.py b/tests/xlfunctions_vs_excel/sqrt_test.py index 6e6fba2..ef9741e 100644 --- a/tests/xlfunctions_vs_excel/sqrt_test.py +++ b/tests/xlfunctions_vs_excel/sqrt_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing from xlcalculator.xlfunctions import xlerrors diff --git a/tests/xlfunctions_vs_excel/sum_test.py b/tests/xlfunctions_vs_excel/sum_test.py index 204b5da..dc65778 100644 --- a/tests/xlfunctions_vs_excel/sum_test.py +++ b/tests/xlfunctions_vs_excel/sum_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class SumTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/sumifs_test.py b/tests/xlfunctions_vs_excel/sumifs_test.py index 175bbdf..e68c800 100644 --- a/tests/xlfunctions_vs_excel/sumifs_test.py +++ b/tests/xlfunctions_vs_excel/sumifs_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class SumIfsTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/sumproduct_test.py b/tests/xlfunctions_vs_excel/sumproduct_test.py index 48a2def..f8e3959 100644 --- a/tests/xlfunctions_vs_excel/sumproduct_test.py +++ b/tests/xlfunctions_vs_excel/sumproduct_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class SumProductTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/test_logical.py b/tests/xlfunctions_vs_excel/test_logical.py index 15e1a62..a6a634d 100644 --- a/tests/xlfunctions_vs_excel/test_logical.py +++ b/tests/xlfunctions_vs_excel/test_logical.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class LogicalFunctionsTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/vlookup_test.py b/tests/xlfunctions_vs_excel/vlookup_test.py index 10fabe4..cec182f 100644 --- a/tests/xlfunctions_vs_excel/vlookup_test.py +++ b/tests/xlfunctions_vs_excel/vlookup_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class VLookupTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/xnpv_test.py b/tests/xlfunctions_vs_excel/xnpv_test.py index e816251..de02e58 100644 --- a/tests/xlfunctions_vs_excel/xnpv_test.py +++ b/tests/xlfunctions_vs_excel/xnpv_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class NPVTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/year_test.py b/tests/xlfunctions_vs_excel/year_test.py index 963ffa6..43754bd 100644 --- a/tests/xlfunctions_vs_excel/year_test.py +++ b/tests/xlfunctions_vs_excel/year_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class YearTest(testing.FunctionalTestCase): diff --git a/tests/xlfunctions_vs_excel/yearfrac_test.py b/tests/xlfunctions_vs_excel/yearfrac_test.py index 98e83a7..b33d802 100644 --- a/tests/xlfunctions_vs_excel/yearfrac_test.py +++ b/tests/xlfunctions_vs_excel/yearfrac_test.py @@ -1,4 +1,4 @@ -from .. import testing +from tests import testing class YearfracTest(testing.FunctionalTestCase): diff --git a/tox.ini b/tox.ini index 8422e4d..856ff1f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,9 @@ envlist = py39, py310, py311, flake8 [gh-actions] python = - 3.9: py39 + 3.9: py39, flake8 3.10: py310, flake8 - 3.11: py311 + 3.11: py311, flake8 [testenv] commands = @@ -23,4 +23,4 @@ skip_install = true deps = flake8 commands = - flake8 xlcalculator/ tests/ + flake8 --ignore=E501,W503 xlcalculator/ tests/ diff --git a/xlcalculator/ast_nodes.py b/xlcalculator/ast_nodes.py index be7f80a..4a78186 100644 --- a/xlcalculator/ast_nodes.py +++ b/xlcalculator/ast_nodes.py @@ -1,22 +1,17 @@ import inspect +from typing import Any, Callable, Union -from xlcalculator.xlfunctions import ( - xl, - xlerrors, - math, - operator, - text, - func_xltypes -) +from xlcalculator.tokenizer import f_token +from xlcalculator.xlfunctions import xl, xlerrors, math, operator, text, func_xltypes -from . import utils +import xlcalculator.utils as utils PREFIX_OP_TO_FUNC = { - '-': operator.OP_NEG, + "-": operator.OP_NEG, } POSTFIX_OP_TO_FUNC = { - '%': operator.OP_PERCENT, + "%": operator.OP_PERCENT, } INFIX_OP_TO_FUNC = { @@ -35,19 +30,22 @@ } MAX_EMPTY = 100 +xl_or_none = Union[func_xltypes.XlAnything, None] +# TODO: what is the purpose of ranges and cells class EvalContext: - - cells = None - ranges = None - namespace = None - seen = None - ref = None - refsheet = None - sheet = None - - def __init__(self, namespace=None, ref=None, seen=None): + cells: dict = None + ranges: dict = None + namespace: dict[str, Callable] = None + seen: list[str] = None + ref: str = None + refsheet: str = None + sheet: str = None + + def __init__( + self, namespace: dict[str, Callable] = None, ref: str = None, seen: list = None + ): self.seen = seen if seen is not None else [] self.namespace = namespace if namespace is not None else xl.FUNCTIONS self.ref = ref @@ -63,31 +61,33 @@ def set_sheet(self, sheet=None): self.sheet = sheet +# TODO: update class to remove object inheritance, no longer needed since like 3.7+? +# TODO: double check the __iter__ I dont think thats correct ... class ASTNode(object): """A generic node in the AST""" - def __init__(self, token): + def __init__(self, token: f_token): self.token = token @property - def tvalue(self): + def tvalue(self) -> str: return self.token.tvalue @property - def ttype(self): + def ttype(self) -> str: return self.token.ttype @property - def tsubtype(self): + def tsubtype(self) -> str: return self.token.tsubtype - def __eq__(self, other): + def __eq__(self, other) -> bool: return self.token == other.token def eval(self, context): - raise NotImplementedError(f'`eval()` of {self}') + raise NotImplementedError(f"`eval()` of {self}") - def __repr__(self): + def __repr__(self) -> str: return ( f"<{self.__class__.__name__} " f"tvalue: {repr(self.tvalue)}, " @@ -96,30 +96,30 @@ def __repr__(self): f">" ) - def __str__(self): + def __str__(self) -> str: return str(self.tvalue) def __iter__(self): yield self +# TODO: why in the world would you return an error and not raise it? what is this? rust? class OperandNode(ASTNode): - def eval(self, context): if self.tsubtype == "logical": return func_xltypes.Boolean.cast(self.tvalue) - elif self.tsubtype == 'text': + elif self.tsubtype == "text": return func_xltypes.Text(self.tvalue) - elif self.tsubtype == 'error': + elif self.tsubtype == "error": if self.tvalue in xlerrors.ERRORS_BY_CODE: return xlerrors.ERRORS_BY_CODE[self.tvalue]( - f'Error in cell ${context.ref}') - return xlerrors.ExcelError( - self.tvalue, f'Error in cell ${context.ref}') + f"Error in cell ${context.ref}" + ) + return xlerrors.ExcelError(self.tvalue, f"Error in cell ${context.ref}") else: return func_xltypes.Number.cast(self.tvalue) - def __str__(self): + def __str__(self) -> str: if self.tsubtype == "logical": return self.tvalue.title() elif self.tsubtype == "text": @@ -130,21 +130,23 @@ def __str__(self): class RangeNode(OperandNode): """Represents a spreadsheet cell, range, named_range.""" + # TODO: check the return, that seems odd def get_cells(self): - cells = utils.resolve_ranges(self.tvalue, default_sheet='')[1] + cells = utils.resolve_ranges(self.tvalue, default_sheet="")[1] return cells[0] if len(cells) == 1 else cells @property - def address(self): + def address(self) -> str: return self.tvalue - def full_address(self, context): + def full_address(self, context: EvalContext) -> str: addr = self.address - if '!' not in addr: - addr = f'{context.sheet}!{addr}' + if "!" not in addr: + addr = f"{context.sheet}!{addr}" return addr - def eval(self, context): + # TODO: remove the Any and figure out the proper return types? xlAnything? + def eval(self, context: EvalContext) -> Union[func_xltypes.Array, Any]: addr = self.full_address(context) if addr in context.ranges: @@ -155,7 +157,7 @@ def eval(self, context): row_cells = [] for col_addr in range_row: cell = context.eval_cell(col_addr) - if cell.value == '' or cell.value is None: + if cell.value == "" or cell.value is None: empty_col += 1 if empty_col > MAX_EMPTY: break @@ -178,35 +180,34 @@ def eval(self, context): class OperatorNode(ASTNode): - - def __init__(self, token): + def __init__(self, token: f_token) -> None: super().__init__(token) - self.left = None - self.right = None + self.left: xl_or_none = None + self.right: xl_or_none = None - def eval(self, context): - if self.ttype == 'operator-prefix': - assert self.left is None, 'Left operand for prefix operator' + def eval(self, context: EvalContext) -> func_xltypes.XlAnything: + if self.ttype == "operator-prefix": + assert self.left is None, "Left operand for prefix operator" op = PREFIX_OP_TO_FUNC[self.tvalue] return op(self.right.eval(context)) - elif self.ttype == 'operator-infix': + elif self.ttype == "operator-infix": op = INFIX_OP_TO_FUNC[self.tvalue] return op( self.left.eval(context), self.right.eval(context), ) - elif self.ttype == 'operator-postfix': - assert self.right is None, 'Right operand for postfix operator' + elif self.ttype == "operator-postfix": + assert self.right is None, "Right operand for postfix operator" op = POSTFIX_OP_TO_FUNC[self.tvalue] return op(self.left.eval(context)) else: - raise ValueError(f'Invalid operator type: {self.ttype}') + raise ValueError(f"Invalid operator type: {self.ttype}") - def __str__(self): - left = f'({self.left}) ' if self.left is not None else '' - right = f' ({self.right})' if self.right is not None else '' - return f'{left}{self.tvalue}{right}' + def __str__(self) -> str: + left = f"({self.left}) " if self.left is not None else "" + right = f" ({self.right})" if self.right is not None else "" + return f"{left}{self.tvalue}{right}" def __iter__(self): # Return node in resolution order. @@ -218,15 +219,15 @@ def __iter__(self): class FunctionNode(ASTNode): """AST node representing a function call""" - def __init__(self, token): + def __init__(self, token: f_token): super().__init__(token) self.args = None - def eval(self, context): + def eval(self, context: EvalContext) -> func_xltypes.XlAnything: func_name = self.tvalue.upper() # 1. Remove the BBB namespace, since we are just supporting # everything in one large one. - func_name = func_name.replace('_XLFN.', '') + func_name = func_name.replace("_XLFN.", "") # 2. Look up the function to use. func = context.namespace[func_name] # 3. Prepare arguments. @@ -237,29 +238,32 @@ def eval(self, context): param = sig.parameters[pname] ptype = param.annotation if ptype == func_xltypes.XlExpr: - args.append(func_xltypes.Expr( - pvalue.eval, (context,), ref=context.ref, ast=pvalue - )) - elif (param.kind == param.VAR_POSITIONAL - and func_xltypes.XlExpr in getattr(ptype, '__args__', [])): - args.extend([ + args.append( func_xltypes.Expr( - pitem.eval, (context,), ref=context.ref, ast=pitem + pvalue.eval, (context,), ref=context.ref, ast=pvalue ) - for pitem in pvalue - ]) - elif (param.kind == param.VAR_POSITIONAL): - args.extend([ - pitem.eval(context) for pitem in pvalue - ]) + ) + elif param.kind == param.VAR_POSITIONAL and func_xltypes.XlExpr in getattr( + ptype, "__args__", [] + ): + args.extend( + [ + func_xltypes.Expr( + pitem.eval, (context,), ref=context.ref, ast=pitem + ) + for pitem in pvalue + ] + ) + elif param.kind == param.VAR_POSITIONAL: + args.extend([pitem.eval(context) for pitem in pvalue]) else: args.append(pvalue.eval(context)) # 4. Run function and return result. return func(*args) - def __str__(self): - args = ', '.join(str(arg) for arg in self.args) - return f'{self.tvalue}({args})' + def __str__(self) -> str: + args = ", ".join(str(arg) for arg in self.args) + return f"{self.tvalue}({args})" def __iter__(self): # Return node in resolution order. diff --git a/xlcalculator/model.py b/xlcalculator/model.py index a62c09b..bbf855d 100644 --- a/xlcalculator/model.py +++ b/xlcalculator/model.py @@ -1,5 +1,6 @@ import copy import gzip +from typing import Union import jsonpickle import logging import os @@ -7,20 +8,25 @@ from . import xltypes, reader, parser, tokenizer +# TODO: change the logging to a module specific one, not root -@dataclass -class Model(): +@dataclass +class Model: cells: dict = field( - init=False, default_factory=dict, compare=True, hash=True, repr=True) + init=False, default_factory=dict, compare=True, hash=True, repr=True + ) formulae: dict = field( - init=False, default_factory=dict, compare=True, hash=True, repr=True) + init=False, default_factory=dict, compare=True, hash=True, repr=True + ) ranges: dict = field( - init=False, default_factory=dict, compare=True, hash=True, repr=True) + init=False, default_factory=dict, compare=True, hash=True, repr=True + ) defined_names: dict = field( - init=False, default_factory=dict, compare=True, hash=True, repr=True) + init=False, default_factory=dict, compare=True, hash=True, repr=True + ) - def set_cell_value(self, address, value): + def set_cell_value(self, address: Union[xltypes.XLCell, str], value) -> None: """Sets a new value for a specified cell.""" if address in self.defined_names: if isinstance(self.defined_names[address], xltypes.XLCell): @@ -45,7 +51,7 @@ def set_cell_value(self, address, value): f"{address}. XLCell or a string is needed." ) - def get_cell_value(self, address): + def get_cell_value(self, address: Union[xltypes.XLCell, str]): if address in self.defined_names: if isinstance(self.defined_names[address], xltypes.XLCell): address = self.defined_names[address].address @@ -56,7 +62,8 @@ def get_cell_value(self, address): else: logging.debug( "Trying to get value for cell {address} but that cell " - "doesn't exist.") + "doesn't exist." + ) return 0 elif isinstance(address, xltypes.XLCell): @@ -65,7 +72,8 @@ def get_cell_value(self, address): else: logging.debug( "Trying to get value for cell {address.address} but " - "that cell doesn't exist") + "that cell doesn't exist" + ) return 0 else: @@ -74,80 +82,84 @@ def get_cell_value(self, address): f"{address}. XLCell or a string is needed." ) - def persist_to_json_file(self, fname): + def persist_to_json_file(self, fname: str) -> None: """Writes the state to disk. Doesn't write the graph directly, but persist all the things that provide the ability to re-create the graph. """ output = { - 'cells': self.cells, - 'defined_names': self.defined_names, - 'formulae': self.formulae, - 'ranges': self.ranges, + "cells": self.cells, + "defined_names": self.defined_names, + "formulae": self.formulae, + "ranges": self.ranges, } - file_open = gzip.GzipFile \ - if os.path.splitext(fname)[-1].lower() in ['.gzip', '.gz'] \ + file_open = ( + gzip.GzipFile + if os.path.splitext(fname)[-1].lower() in [".gzip", ".gz"] else open + ) - with file_open(fname, 'wb') as fp: + with file_open(fname, "wb") as fp: fp.write(jsonpickle.encode(output, keys=True).encode()) - def construct_from_json_file(self, fname, build_code=False): + def construct_from_json_file(self, fname: str, build_code: bool = False) -> None: """Constructs a graph from a state persisted to disk.""" - file_open = gzip.GzipFile \ - if os.path.splitext(fname)[-1].lower() in ['.gzip', '.gz'] \ + file_open = ( + gzip.GzipFile + if os.path.splitext(fname)[-1].lower() in [".gzip", ".gz"] else open + ) with file_open(fname, "rb") as fp: json_bytes = fp.read() data = jsonpickle.decode( - json_bytes, keys=True, + json_bytes, + keys=True, classes=( - xltypes.XLCell, xltypes.XLFormula, xltypes.XLRange, - tokenizer.f_token - ) + xltypes.XLCell, + xltypes.XLFormula, + xltypes.XLRange, + tokenizer.f_token, + ), ) - self.cells = data['cells'] + self.cells = data["cells"] - self.defined_names = data['defined_names'] - self.ranges = data['ranges'] - self.formulae = data['formulae'] + self.defined_names = data["defined_names"] + self.ranges = data["ranges"] + self.formulae = data["formulae"] if build_code: self.build_code() - def build_code(self): + def build_code(self) -> None: """Define the Python code for all cells in the dict of cells.""" for cell in self.cells: if self.cells[cell].formula is not None: defined_names = { - name: defn.address - for name, defn in self.defined_names.items()} + name: defn.address for name, defn in self.defined_names.items() + } self.cells[cell].formula.ast = parser.FormulaParser().parse( - self.cells[cell].formula.formula, defined_names) - - def __eq__(self, other): + self.cells[cell].formula.formula, defined_names + ) + def __eq__(self, other) -> bool: cells_comparison = [] for self_cell in self.cells: - cells_comparison.append( - self.cells[self_cell] == other.cells[self_cell]) + cells_comparison.append(self.cells[self_cell] == other.cells[self_cell]) defined_names_comparison = [] for self_defined_names in self.defined_names: defined_names_comparison.append( - self.defined_names[self_defined_names] - == other.defined_names[self_defined_names]) + self.defined_names[self_defined_names] == other.defined_names[self_defined_names] + ) return ( - self.__class__ == other.__class__ - and all(cells_comparison) - and all(defined_names_comparison) + self.__class__ == other.__class__ and all(cells_comparison) and all(defined_names_comparison) ) @@ -162,27 +174,37 @@ class ModelCompiler: def __init__(self): self.model = Model() - def read_excel_file(self, file_name): + def read_excel_file(self, file_name: Union[str, os.PathLike]) -> reader.Reader: archive = reader.Reader(file_name) archive.read() return archive - def parse_archive(self, archive, ignore_sheets=[], ignore_hidden=False): - self.model.cells, self.model.formulae, self.model.ranges = \ - archive.read_cells(ignore_sheets, ignore_hidden) - self.defined_names = archive.read_defined_names( - ignore_sheets, ignore_hidden) + def parse_archive( + self, + archive: reader.Reader, + ignore_sheets: list[str] = [], + ignore_hidden: bool = False, + ) -> None: + self.model.cells, self.model.formulae, self.model.ranges = archive.read_cells( + ignore_sheets, ignore_hidden + ) + self.defined_names = archive.read_defined_names(ignore_sheets, ignore_hidden) self.build_defined_names() self.link_cells_to_defined_names() self.build_ranges() + # TODO: why does file name have a default value that will crash things? Recommend setting it as a required value def read_and_parse_archive( - self, file_name=None, ignore_sheets=[], ignore_hidden=False, - build_code=True - ): + self, + file_name: Union[str, os.PathLike] = None, + ignore_sheets: list[str] = [], + ignore_hidden: bool = False, + build_code: bool = True, + ) -> Model: archive = self.read_excel_file(file_name) self.parse_archive( - archive, ignore_sheets=ignore_sheets, ignore_hidden=ignore_hidden) + archive, ignore_sheets=ignore_sheets, ignore_hidden=ignore_hidden + ) if build_code: self.model.build_code() @@ -190,30 +212,24 @@ def read_and_parse_archive( return self.model def read_and_parse_dict( - self, input_dict, default_sheet="Sheet1", build_code=True): + self, input_dict: dict, default_sheet: str = "Sheet1", build_code: bool = True + ) -> Model: for item in input_dict: if "!" in item: cell_address = item else: cell_address = "{}!{}".format(default_sheet, item) - if ( - not isinstance(input_dict[item], (float, int)) - and input_dict[item][0] == '=' - ): - formula = xltypes.XLFormula( - input_dict[item], - sheet_name=default_sheet - ) - cell = xltypes.XLCell( - cell_address, None, - formula=formula) + if (not isinstance(input_dict[item], (float, int)) and input_dict[item][0] == "="): + formula = xltypes.XLFormula(input_dict[item], sheet_name=default_sheet) + cell = xltypes.XLCell(cell_address, None, formula=formula) self.model.cells[cell_address] = cell self.model.formulae[cell_address] = cell.formula else: self.model.cells[cell_address] = xltypes.XLCell( - cell_address, input_dict[item]) + cell_address, input_dict[item] + ) self.build_ranges(default_sheet=default_sheet) @@ -222,39 +238,35 @@ def read_and_parse_dict( return self.model - def build_defined_names(self): + def build_defined_names(self) -> None: """Add defined ranges to model.""" for name in self.defined_names: cell_address = self.defined_names[name] - cell_address = cell_address.replace('$', '') + cell_address = cell_address.replace("$", "") # a cell has an address like; Sheet1!A1 - if ':' not in cell_address: + if ":" not in cell_address: if cell_address not in self.model.cells: logging.warning( f"Defined name {name} refers to empty cell " - f"{cell_address}. Is not being loaded.") + f"{cell_address}. Is not being loaded." + ) continue else: if self.model.cells[cell_address] is not None: - self.model.defined_names[name] = \ - self.model.cells[cell_address] + self.model.defined_names[name] = self.model.cells[cell_address] else: self.model.defined_names[name] = xltypes.XLRange( - cell_address, name=name) - self.model.ranges[cell_address] = \ - self.model.defined_names[name] - - if ( - cell_address in self.model.formulae - and name not in self.model.formulae - ): - self.model.formulae[name] = \ - self.model.cells[cell_address].formula - - def link_cells_to_defined_names(self): + cell_address, name=name + ) + self.model.ranges[cell_address] = self.model.defined_names[name] + + if cell_address in self.model.formulae and name not in self.model.formulae: + self.model.formulae[name] = self.model.cells[cell_address].formula + + def link_cells_to_defined_names(self) -> None: for name in self.model.defined_names: defn = self.model.defined_names[name] @@ -265,8 +277,7 @@ def link_cells_to_defined_names(self): if any(isinstance(el, list) for el in defn.cells): for column in defn.cells: for row_address in column: - self.model.cells[row_address].defined_names.append( - name) + self.model.cells[row_address].defined_names.append(name) else: # programmer error message = "This isn't a dim2 array. {}".format(name) @@ -280,7 +291,8 @@ def link_cells_to_defined_names(self): logging.error(message) raise ValueError(message) - def build_ranges(self, default_sheet=None): + # TODO: replace default sheet with a valid sheet name if one is not provided, there is no checks in place + def build_ranges(self, default_sheet: str = None) -> None: for formula in self.model.formulae: associated_cells = set() for range in self.model.formulae[formula].terms: @@ -288,11 +300,13 @@ def build_ranges(self, default_sheet=None): if "!" not in range: range = "{}!{}".format(default_sheet, range) self.model.ranges[range] = xltypes.XLRange(range, range) - associated_cells.update([ - cell - for row in self.model.ranges[range].cells + associated_cells.update( + [ + cell + for row in self.model.ranges[range].cells for cell in row # noqa: E131 - ]) + ] + ) else: associated_cells.add(range) @@ -300,49 +314,50 @@ def build_ranges(self, default_sheet=None): for row in self.model.ranges[range].cells: for cell_address in row: if cell_address not in self.model.cells.keys(): - self.model.cells[cell_address] = \ - xltypes.XLCell(cell_address, '') + self.model.cells[cell_address] = xltypes.XLCell( + cell_address, "" + ) if formula in self.model.cells: - self.model.cells[formula].formula.associated_cells = \ - associated_cells + self.model.cells[formula].formula.associated_cells = associated_cells if formula in self.model.defined_names: - self.model.defined_names[formula].formula.associated_cells = \ - associated_cells + self.model.defined_names[ + formula + ].formula.associated_cells = associated_cells self.model.formulae[formula].associated_cells = associated_cells @staticmethod - def extract(model, focus): + def extract(model: Model, focus: list[str]) -> Model: extracted_model = Model() for address in focus: if isinstance(address, str) and address in model.cells: - extracted_model.cells[address] = copy.deepcopy( - model.cells[address]) + extracted_model.cells[address] = copy.deepcopy(model.cells[address]) elif isinstance(address, str) and address in model.defined_names: - extracted_model.defined_names[address] = defn = copy.deepcopy( - model.defined_names[address]) + model.defined_names[address] + ) if isinstance(defn, xltypes.XLCell): extracted_model.cells[defn.address] = copy.deepcopy( - model.cells[defn.address]) + model.cells[defn.address] + ) elif isinstance(defn, xltypes.XLRange): for row in defn.cells: for column in row: extracted_model.cells[column] = copy.deepcopy( - model.cells[column]) + model.cells[column] + ) terms_to_copy = [] for addr, cell in extracted_model.cells.items(): if cell.formula is not None: for term in cell.formula.terms: - if (term in extracted_model.cells - and cell.formula != model.cells[addr].formula): + if (term in extracted_model.cells and cell.formula != model.cells[addr].formula): cell.formula = copy.deepcopy(model.cells[addr].formula) elif term not in extracted_model.cells: diff --git a/xlcalculator/parser.py b/xlcalculator/parser.py index 48bd064..46a9e12 100644 --- a/xlcalculator/parser.py +++ b/xlcalculator/parser.py @@ -10,35 +10,38 @@ def __init__(self, value, precedence, associativity): self.associativity = associativity +# TODO: why is this like the third time that the operators are defined? + # http://office.microsoft.com/en-us/excel-help/ # calculation-operators-and-precedence-HP010078886.aspx OPERATORS = { - ':': Operator(':', 8, 'left'), - '': Operator(' ', 8, 'left'), - ',': Operator(',', 8, 'left'), - 'u-': Operator('u-', 7, 'right'), # unary negation - '%': Operator('%', 6, 'left'), - '^': Operator('^', 5, 'left'), - '*': Operator('*', 4, 'left'), - '/': Operator('/', 4, 'left'), - '+': Operator('+', 3, 'left'), - '-': Operator('-', 3, 'left'), - '&': Operator('&', 2, 'left'), - '=': Operator('=', 1, 'left'), - '<': Operator('<', 1, 'left'), - '>': Operator('>', 1, 'left'), - '<=': Operator('<=', 1, 'left'), - '>=': Operator('>=', 1, 'left'), - '<>': Operator('<>', 1, 'left'), + ":": Operator(":", 8, "left"), + "": Operator(" ", 8, "left"), + ",": Operator(",", 8, "left"), + "u-": Operator("u-", 7, "right"), # unary negation + "%": Operator("%", 6, "left"), + "^": Operator("^", 5, "left"), + "*": Operator("*", 4, "left"), + "/": Operator("/", 4, "left"), + "+": Operator("+", 3, "left"), + "-": Operator("-", 3, "left"), + "&": Operator("&", 2, "left"), + "=": Operator("=", 1, "left"), + "<": Operator("<", 1, "left"), + ">": Operator(">", 1, "left"), + "<=": Operator("<=", 1, "left"), + ">=": Operator(">=", 1, "left"), + "<>": Operator("<>", 1, "left"), } class FormulaParser: """Excel Formula Parser""" - def parse(self, formula, named_ranges=None, tokenize_range=False): - """Parse formula into evaluable AST. - """ + # TODO: Changed the default for named ranges to something that dons't crash + # TODO: deal with unused variables + def parse(self, formula: str, named_ranges=None, tokenize_range=False): + """Parse formula into evaluable AST.""" # 1. Parse the formula into syntactic tokens. tokens = self.tokenize(formula) # 2. Organize tokens into reverse polish notation. @@ -47,15 +50,22 @@ def parse(self, formula, named_ranges=None, tokenize_range=False): ast = self.build_ast(nodes) return ast - def tokenize(self, formula, tokenize_range=False): + def tokenize( + self, formula: str, tokenize_range: bool = False + ) -> list[tokenizer.f_token]: # Remove leading "=" sign. - if formula.startswith('='): + if formula.startswith("="): formula = formula[1:] excel_parser = tokenizer.ExcelParser(tokenize_range=tokenize_range) return excel_parser.parse(formula).items - def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): + def shunting_yard( + self, + raw_tokens: list[tokenizer.f_token], + named_ranges: list[str], + tokenize_range: bool = False, + ) -> list[ast_nodes.ASTNode]: """Reorganize tokens into proper reverse polish notation. Core algorithm taken from wikipedia with varargs extensions from @@ -85,21 +95,20 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): if token.ttype == "function" and token.tsubtype == "start": token.tsubtype = "" tokens.append(token) - tokens.append(tokenizer.f_token('(', 'arglist', 'start')) + tokens.append(tokenizer.f_token("(", "arglist", "start")) elif token.ttype == "function" and token.tsubtype == "stop": - tokens.append(tokenizer.f_token(')', 'arglist', 'stop')) + tokens.append(tokenizer.f_token(")", "arglist", "stop")) elif token.ttype == "subexpression" and token.tsubtype == "start": - token.tvalue = '(' + token.tvalue = "(" tokens.append(token) elif token.ttype == "subexpression" and token.tsubtype == "stop": - token.tvalue = ')' + token.tvalue = ")" tokens.append(token) - elif (token.ttype == "operand" and token.tsubtype == "range" - and token.tvalue in named_ranges): + elif (token.ttype == "operand" and token.tsubtype == "range" and token.tvalue in named_ranges): # Resolve the named range once and for all. token.tvalue = named_ranges[token.tvalue] tokens.append(token) @@ -120,23 +129,19 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): for index, token in enumerate(tokens): new_tokens.append(token) - if type(token.tvalue) == str: - + if type(token.tvalue) is str: # example -> :OFFSET( or simply :A10 - if token.tvalue.startswith(':'): + if token.tvalue.startswith(":"): depth = 0 - expr = '' + expr = "" rev = reversed(tokens[:index]) # going backwards, 'stop' starts, 'start' stops for reversed_token in rev: - if reversed_token.tsubtype == 'stop': + if reversed_token.tsubtype == "stop": depth += 1 - elif ( - depth > 0 - and reversed_token.tsubtype == 'start' - ): + elif depth > 0 and reversed_token.tsubtype == "start": depth -= 1 expr = reversed_token.tvalue + expr @@ -154,12 +159,12 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): depth = 0 - if token.tvalue[1:] in ['OFFSET', 'INDEX']: + if token.tvalue[1:] in ["OFFSET", "INDEX"]: for t in tokens[(index + 1):]: - if t.tsubtype == 'start': + if t.tsubtype == "start": depth += 1 - elif depth > 0 and t.tsubtype == 'stop': + elif depth > 0 and t.tsubtype == "stop": depth -= 1 expr += t.tvalue @@ -168,20 +173,19 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): if depth == 0: break - new_tokens.append( - tokenizer.f_token(expr, 'operand', 'pointer')) + new_tokens.append(tokenizer.f_token(expr, "operand", "pointer")) # example -> A1:OFFSET( - elif ':OFFSET' in token.tvalue or ':INDEX' in token.tvalue: + elif ":OFFSET" in token.tvalue or ":INDEX" in token.tvalue: depth = 0 - expr = '' + expr = "" expr += token.tvalue for t in tokens[(index + 1):]: - if t.tsubtype == 'start': + if t.tsubtype == "start": depth += 1 - elif t.tsubtype == 'stop': + elif t.tsubtype == "stop": depth -= 1 expr += t.tvalue @@ -191,8 +195,7 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): new_tokens.pop() break - new_tokens.append( - tokenizer.f_token(expr, 'operand', 'pointer')) + new_tokens.append(tokenizer.f_token(expr, "operand", "pointer")) tokens = new_tokens if new_tokens else tokens @@ -215,7 +218,6 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): were_values.append(False) elif token.ttype == "argument": - while stack and (stack[-1].tsubtype != "start"): output.append(self.create_node(stack.pop())) @@ -226,32 +228,26 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): if not len(stack): raise ValueError("Mismatched or misplaced parentheses") - elif token.ttype.startswith('operator'): - - if token.ttype.endswith('-prefix') and token.tvalue == "-": - o1 = OPERATORS['u-'] + elif token.ttype.startswith("operator"): + if token.ttype.endswith("-prefix") and token.tvalue == "-": + o1 = OPERATORS["u-"] else: o1 = OPERATORS[token.tvalue] - while stack and stack[-1].ttype.startswith('operator'): - if ( - stack[-1].ttype.endswith('-prefix') - and stack[-1].tvalue == "-" - ): - o2 = OPERATORS['u-'] + while stack and stack[-1].ttype.startswith("operator"): + if stack[-1].ttype.endswith("-prefix") and stack[-1].tvalue == "-": + o2 = OPERATORS["u-"] else: o2 = OPERATORS[stack[-1].tvalue] if ( - (o1.associativity == "left" - and o1.precedence <= o2.precedence) - or (o1.associativity == "right" - and o1.precedence < o2.precedence) + o1.associativity == "left" and o1.precedence <= o2.precedence + ) or ( + o1.associativity == "right" and o1.precedence < o2.precedence ): - output.append( - self.create_node(stack.pop())) + output.append(self.create_node(stack.pop())) else: break @@ -280,7 +276,7 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): output.append(f) while stack: - if (stack[-1].tsubtype == "start" or stack[-1].tsubtype == "stop"): + if stack[-1].tsubtype == "start" or stack[-1].tsubtype == "stop": raise SyntaxError("Mismatched or misplaced parentheses") output.append(self.create_node(stack.pop())) @@ -288,7 +284,7 @@ def shunting_yard(self, raw_tokens, named_ranges, tokenize_range=False): # convert to list return [x for x in output] - def create_node(self, token): + def create_node(self, token: tokenizer.f_token) -> ast_nodes.ASTNode: if token.ttype == "operand": if token.tsubtype in ["range", "pointer"]: return ast_nodes.RangeNode(token) @@ -302,15 +298,15 @@ def create_node(self, token): return ast_nodes.OperatorNode(token) else: - raise ValueError('Unknown token type: ' + token.ttype) + raise ValueError("Unknown token type: " + token.ttype) - def build_ast(self, nodes): + def build_ast(self, nodes: list[ast_nodes.ASTNode]) -> ast_nodes.ASTNode: """Update AST nodes to build a proper parse tree. XXX: There is really no need for this. The shunting yeard algorithm should jsut take care of it. """ - stack = [] + stack: list[ast_nodes.ASTNode] = [] for node in nodes: if isinstance(node, ast_nodes.OperatorNode): diff --git a/xlcalculator/reader.py b/xlcalculator/reader.py index f763f8b..8673c0c 100644 --- a/xlcalculator/reader.py +++ b/xlcalculator/reader.py @@ -1,40 +1,42 @@ +from os import PathLike +from typing import Union import openpyxl from . import patch, xltypes +# TODO: remove unused variables -class Reader(): - def __init__(self, file_name): +class Reader: + def __init__(self, file_name: Union[str, PathLike]): self.excel_file_name = file_name - def read(self): + def read(self) -> None: with patch.openpyxl_WorksheetReader_patch(): self.book = openpyxl.load_workbook(self.excel_file_name) - def read_defined_names(self, ignore_sheets=[], ignore_hidden=False): + def read_defined_names(self, ignore_sheets=[], ignore_hidden=False) -> dict: return { defn.name: defn.value for name, defn in self.book.defined_names.items() - if defn.hidden is None and defn.value != '#REF!' + if defn.hidden is None and defn.value != "#REF!" } - def read_cells(self, ignore_sheets=[], ignore_hidden=False): - cells = {} - formulae = {} - ranges = {} + def read_cells( + self, ignore_sheets: list[str] = [], ignore_hidden=False + ) -> tuple[dict[str, xltypes.XLCell], dict[str, xltypes.XLFormula], set]: + cells: dict[str, xltypes.XLCell] = {} + formulae: dict[str, xltypes.XLFormula] = {} + ranges: set = {} # TODO: what is this for? for sheet_name in self.book.sheetnames: if sheet_name in ignore_sheets: continue sheet = self.book[sheet_name] for cell in sheet._cells.values(): - addr = f'{sheet_name}!{cell.coordinate}' - if cell.data_type == 'f': + addr = f"{sheet_name}!{cell.coordinate}" + if cell.data_type == "f": value = cell.value - if isinstance( - value, - openpyxl.worksheet.formula.ArrayFormula - ): + if isinstance(value, openpyxl.worksheet.formula.ArrayFormula): value = value.text formula = xltypes.XLFormula(value, sheet_name) formulae[addr] = formula @@ -43,7 +45,6 @@ def read_cells(self, ignore_sheets=[], ignore_hidden=False): formula = None value = cell.value - cells[addr] = xltypes.XLCell( - addr, value=value, formula=formula) + cells[addr] = xltypes.XLCell(addr, value=value, formula=formula) return [cells, formulae, ranges] diff --git a/xlcalculator/tokenizer.py b/xlcalculator/tokenizer.py index 967a8c2..b2e5532 100644 --- a/xlcalculator/tokenizer.py +++ b/xlcalculator/tokenizer.py @@ -21,28 +21,29 @@ import re from dataclasses import dataclass, field +from typing import Union import uuid from string import ascii_uppercase -def col2num(col): +def col2num(col) -> int: if not col: raise Exception("Column may not be empty") tot = 0 for i, c in enumerate([c for c in col[::-1] if c != "$"]): - if c == '$': + if c == "$": continue - tot += (ord(c) - 64) * 26 ** i + tot += (ord(c) - 64) * 26**i return tot -def num2col(num): +def num2col(num) -> str: if num < 1: raise Exception("Number must be larger than 0: %s" % num) - s = '' + s = "" q = num while q > 0: (q, r) = divmod(q, 26) @@ -63,32 +64,32 @@ def num2col(num): # Methods: None # ======================================================================== class ExcelParserTokens(object): - TOK_TYPE_NOOP = "noop" - TOK_TYPE_OPERAND = "operand" - TOK_TYPE_FUNCTION = "function" - TOK_TYPE_SUBEXPR = "subexpression" - TOK_TYPE_ARGUMENT = "argument" - TOK_TYPE_OP_PRE = "operator-prefix" - TOK_TYPE_OP_IN = "operator-infix" - TOK_TYPE_OP_POST = "operator-postfix" - TOK_TYPE_WSPACE = "white-space" - TOK_TYPE_UNKNOWN = "unknown" - - TOK_SUBTYPE_START = "start" - TOK_SUBTYPE_STOP = "stop" - TOK_SUBTYPE_TEXT = "text" - TOK_SUBTYPE_NUMBER = "number" - TOK_SUBTYPE_LOGICAL = "logical" - TOK_SUBTYPE_ERROR = "error" - TOK_SUBTYPE_RANGE = "range" - TOK_SUBTYPE_MATH = "math" - TOK_SUBTYPE_CONCAT = "concatenate" - TOK_SUBTYPE_INTERSECT = "intersect" - TOK_SUBTYPE_UNION = "union" - TOK_SUBTYPE_NONE = "none" - - -def init_uuid(): + TOK_TYPE_NOOP: str = "noop" + TOK_TYPE_OPERAND: str = "operand" + TOK_TYPE_FUNCTION: str = "function" + TOK_TYPE_SUBEXPR: str = "subexpression" + TOK_TYPE_ARGUMENT: str = "argument" + TOK_TYPE_OP_PRE: str = "operator-prefix" + TOK_TYPE_OP_IN: str = "operator-infix" + TOK_TYPE_OP_POST: str = "operator-postfix" + TOK_TYPE_WSPACE: str = "white-space" + TOK_TYPE_UNKNOWN: str = "unknown" + + TOK_SUBTYPE_START: str = "start" + TOK_SUBTYPE_STOP: str = "stop" + TOK_SUBTYPE_TEXT: str = "text" + TOK_SUBTYPE_NUMBER: str = "number" + TOK_SUBTYPE_LOGICAL: str = "logical" + TOK_SUBTYPE_ERROR: str = "error" + TOK_SUBTYPE_RANGE: str = "range" + TOK_SUBTYPE_MATH: str = "math" + TOK_SUBTYPE_CONCAT: str = "concatenate" + TOK_SUBTYPE_INTERSECT: str = "intersect" + TOK_SUBTYPE_UNION: str = "union" + TOK_SUBTYPE_NONE: str = "none" + + +def init_uuid() -> uuid.UUID: """Default factory to initialise Formula.ranges.""" return uuid.uuid4() @@ -105,17 +106,17 @@ def init_uuid(): # ======================================================================== @dataclass class f_token: - tvalue: str ttype: str tsubtype: str unique_identifier: uuid = field( - init=False, default_factory=init_uuid, compare=True, hash=True, - repr=True) + init=False, default_factory=init_uuid, compare=True, hash=True, repr=True + ) def __repr__(self): return "<{} tvalue: {} ttype: {} tsubtype: {}>".format( - self.__class__.__name__, self.tvalue, self.ttype, self.tsubtype) + self.__class__.__name__, self.tvalue, self.ttype, self.tsubtype + ) def __str__(self): return self.__repr__() @@ -142,47 +143,46 @@ def __str__(self): # the index unchanged) # ======================================================================== class f_tokens(object): - def __init__(self): - self.items = [] + self.items: list[f_token] = [] self.index = -1 - def add(self, value, type, subtype=""): - if (not subtype): + def add(self, value: str, type: str, subtype: str = "") -> f_token: + if not subtype: subtype = "" token = f_token(value, type, subtype) self.addRef(token) return token - def addRef(self, token): + def addRef(self, token: f_token) -> None: self.items.append(token) - def reset(self): + def reset(self) -> None: self.index = -1 - def BOF(self): + def BOF(self) -> bool: return self.index <= 0 - def EOF(self): + def EOF(self) -> bool: return self.index >= (len(self.items) - 1) - def moveNext(self): + def moveNext(self) -> bool: if self.EOF(): return False self.index += 1 return True - def current(self): + def current(self) -> Union[f_token, None]: if self.index == -1: return None return self.items[self.index] - def __next__(self): + def __next__(self) -> Union[f_token, None]: if self.EOF(): return None return self.items[self.index + 1] - def previous(self): + def previous(self) -> Union[f_token, None]: if self.index < 1: return None return self.items[self.index - 1] @@ -213,27 +213,22 @@ def next(self): # String - value() - Return the top token's value # ======================================================================== class f_tokenStack(ExcelParserTokens): - def __init__(self): - self.items = [] + self.items: list[f_token] = [] - def push(self, token): + def push(self, token: f_token) -> None: self.items.append(token) - def pop(self): + def pop(self) -> f_token: token = self.items.pop() return f_token("", token.ttype, self.TOK_SUBTYPE_STOP) def token(self): # Note: this uses Pythons and/or "hack" to emulate C's ternary # operator (i.e. cond ? exp1 : exp2) - return ( - ( - (len(self.items) > 0) - and [self.items[len(self.items) - 1]] - or [None] - )[0] - ) + return ((len(self.items) > 0) and [self.items[len(self.items) - 1]] or [None])[ + 0 + ] def value(self): return ((self.token()) and [(self.token()).tvalue] or [""])[0] @@ -254,22 +249,20 @@ def subtype(self): # Methods: f_tokens - getTokens(formula) - return a token stream (list) # ======================================================================== class ExcelParser(ExcelParserTokens): - - def __init__(self, tokenize_range=False): + def __init__(self, tokenize_range: bool = False): if tokenize_range: - self.OPERATORS = "+-*/^&=><:" + self.OPERATORS: str = "+-*/^&=><:" else: - self.OPERATORS = "+-*/^&=><" - - def getTokens(self, formula): + self.OPERATORS: str = "+-*/^&=><" - def currentChar(): + def getTokens(self, formula: str) -> f_tokens: + def currentChar() -> str: return formula[offset] - def doubleChar(): - return formula[offset:offset + 2] + def doubleChar() -> str: + return formula[offset: offset + 2] - def nextChar(): + def nextChar() -> str: # JavaScript returns an empty string if the index is out of bounds, # Python throws an IndexError. We mimic this behaviour here. try: @@ -291,12 +284,12 @@ def EOF(): inRange = False inError = False - while (len(formula) > 0): - if (formula[0] in (" ", "\n")): + while len(formula) > 0: + if formula[0] in (" ", "\n"): formula = formula[1:] else: - if (formula[0] == "="): + if formula[0] == "=": formula = formula[1:] break @@ -306,16 +299,14 @@ def EOF(): # embeds are doubled # end marks token if inString: - if currentChar() == "\"": - if nextChar() == "\"": - token += "\"" + if currentChar() == '"': + if nextChar() == '"': + token += '"' offset += 1 else: inString = False - tokens.add( - token, self.TOK_TYPE_OPERAND, - self.TOK_SUBTYPE_TEXT) + tokens.add(token, self.TOK_TYPE_OPERAND, self.TOK_SUBTYPE_TEXT) token = "" else: @@ -355,17 +346,15 @@ def EOF(): if inError: token += currentChar() offset += 1 - if ",#NULL!,#DIV/0!,#VALUE!,#REF!,#NAME?,#NUM!,#N/A,".find( - "," + token + ",") != -1: + if (",#NULL!,#DIV/0!,#VALUE!,#REF!,#NAME?,#NUM!,#N/A,".find("," + token + ",") != -1): inError = False - tokens.add( - token, self.TOK_TYPE_OPERAND, self.TOK_SUBTYPE_ERROR) + tokens.add(token, self.TOK_TYPE_OPERAND, self.TOK_SUBTYPE_ERROR) token = "" continue # scientific notation check - regexSN = r'^[1-9]{1}(\.[0-9]+)?[eE]{1}$' - if (("+-").find(currentChar()) != -1): + regexSN = r"^[1-9]{1}(\.[0-9]+)?[eE]{1}$" + if ("+-").find(currentChar()) != -1: if len(token) > 1: if re.match(regexSN, token): token += currentChar() @@ -375,7 +364,7 @@ def EOF(): # independent character evaulation (order not important) # # establish state-dependent character evaluations - if currentChar() == "\"": + if currentChar() == '"': if len(token) > 0: # not expected tokens.add(token, self.TOK_TYPE_UNKNOWN) @@ -393,14 +382,14 @@ def EOF(): offset += 1 continue - if (currentChar() == "["): + if currentChar() == "[": inRange = True token += currentChar() offset += 1 continue - if (currentChar() == "#"): - if (len(token) > 0): + if currentChar() == "#": + if len(token) > 0: # not expected tokens.add(token, self.TOK_TYPE_UNKNOWN) token = "" @@ -410,34 +399,38 @@ def EOF(): continue # mark start and end of arrays and array rows - if (currentChar() == "{"): - if (len(token) > 0): + if currentChar() == "{": + if len(token) > 0: # not expected tokens.add(token, self.TOK_TYPE_UNKNOWN) token = "" - tokenStack.push(tokens.add( - "ARRAY", - self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START)) - tokenStack.push(tokens.add( - "ARRAYROW", - self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START)) + tokenStack.push( + tokens.add("ARRAY", self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START) + ) + tokenStack.push( + tokens.add( + "ARRAYROW", self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START + ) + ) offset += 1 continue - if (currentChar() == ";"): - if (len(token) > 0): + if currentChar() == ";": + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" tokens.addRef(tokenStack.pop()) tokens.add(",", self.TOK_TYPE_ARGUMENT) - tokenStack.push(tokens.add( - "ARRAYROW", - self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START)) + tokenStack.push( + tokens.add( + "ARRAYROW", self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START + ) + ) offset += 1 continue - if (currentChar() == "}"): - if (len(token) > 0): + if currentChar() == "}": + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" tokens.addRef(tokenStack.pop()) @@ -446,30 +439,28 @@ def EOF(): continue # trim white-space - if (currentChar() in (" ", "\n")): - if (len(token) > 0): + if currentChar() in (" ", "\n"): + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" tokens.add("", self.TOK_TYPE_WSPACE) offset += 1 - while ((currentChar() in (" ", "\n")) and (not EOF())): + while (currentChar() in (" ", "\n")) and (not EOF()): offset += 1 continue # multi-character comparators - if (",>=,<=,<>,".find("," + doubleChar() + ",") != -1): - if (len(token) > 0): + if ",>=,<=,<>,".find("," + doubleChar() + ",") != -1: + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" - tokens.add( - doubleChar(), - self.TOK_TYPE_OP_IN, self.TOK_SUBTYPE_LOGICAL) + tokens.add(doubleChar(), self.TOK_TYPE_OP_IN, self.TOK_SUBTYPE_LOGICAL) offset += 2 continue # standard infix operators - if (self.OPERATORS.find(currentChar()) != -1): - if (len(token) > 0): + if self.OPERATORS.find(currentChar()) != -1: + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" tokens.add(currentChar(), self.TOK_TYPE_OP_IN) @@ -477,51 +468,53 @@ def EOF(): continue # standard postfix operators - if ("%".find(currentChar()) != -1): - if (len(token) > 0): + if "%".find(currentChar()) != -1: + if len(token) > 0: tokens.add(float(token) / 100, self.TOK_TYPE_OPERAND) token = "" else: - tokens.add('*', self.TOK_TYPE_OP_IN) + tokens.add("*", self.TOK_TYPE_OP_IN) tokens.add(0.01, self.TOK_TYPE_OPERAND) # tokens.add(currentChar(), self.TOK_TYPE_OP_POST) offset += 1 continue # start subexpression or function - if (currentChar() == "("): - if (len(token) > 0): - tokenStack.push(tokens.add( - token, self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START)) + if currentChar() == "(": + if len(token) > 0: + tokenStack.push( + tokens.add( + token, self.TOK_TYPE_FUNCTION, self.TOK_SUBTYPE_START + ) + ) token = "" else: - tokenStack.push(tokens.add( - "", self.TOK_TYPE_SUBEXPR, self.TOK_SUBTYPE_START)) + tokenStack.push( + tokens.add("", self.TOK_TYPE_SUBEXPR, self.TOK_SUBTYPE_START) + ) offset += 1 continue # function, subexpression, array parameters - if (currentChar() == ","): - if (len(token) > 0): + if currentChar() == ",": + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" - if (not (tokenStack.type() == self.TOK_TYPE_FUNCTION)): + if not (tokenStack.type() == self.TOK_TYPE_FUNCTION): tokens.add( - currentChar(), - self.TOK_TYPE_OP_IN, self.TOK_SUBTYPE_UNION) + currentChar(), self.TOK_TYPE_OP_IN, self.TOK_SUBTYPE_UNION + ) else: tokens.add(currentChar(), self.TOK_TYPE_ARGUMENT) offset += 1 - if (currentChar() == ","): - tokens.add( - 'None', - self.TOK_TYPE_OPERAND, self.TOK_SUBTYPE_NONE) + if currentChar() == ",": + tokens.add("None", self.TOK_TYPE_OPERAND, self.TOK_SUBTYPE_NONE) token = "" continue # stop subexpression - if (currentChar() == ")"): - if (len(token) > 0): + if currentChar() == ")": + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) token = "" tokens.addRef(tokenStack.pop()) @@ -533,49 +526,33 @@ def EOF(): offset += 1 # dump remaining accumulation - if (len(token) > 0): + if len(token) > 0: tokens.add(token, self.TOK_TYPE_OPERAND) # move all tokens to a new collection, excluding all unnecessary # white-space tokens tokens2 = f_tokens() - while (tokens.moveNext()): + while tokens.moveNext(): token = tokens.current() - if (token.ttype == self.TOK_TYPE_WSPACE): - if ((tokens.BOF()) or (tokens.EOF())): + if token.ttype == self.TOK_TYPE_WSPACE: + if (tokens.BOF()) or (tokens.EOF()): pass - elif (not ( - ( - (tokens.previous().ttype == self.TOK_TYPE_FUNCTION) - and (tokens.previous().tsubtype - == self.TOK_SUBTYPE_STOP) - ) or ( - (tokens.previous().ttype == self.TOK_TYPE_SUBEXPR) - and (tokens.previous().tsubtype - == self.TOK_SUBTYPE_STOP) - ) or ( - tokens.previous().ttype == self.TOK_TYPE_OPERAND - ) - )): + elif not ( + ((tokens.previous().ttype == self.TOK_TYPE_FUNCTION) and (tokens.previous().tsubtype == self.TOK_SUBTYPE_STOP)) + or ((tokens.previous().ttype == self.TOK_TYPE_SUBEXPR) and (tokens.previous().tsubtype == self.TOK_SUBTYPE_STOP)) + or (tokens.previous().ttype == self.TOK_TYPE_OPERAND) + ): pass - elif (not ( - ( - (tokens.next().ttype == self.TOK_TYPE_FUNCTION) - and (tokens.next().tsubtype - == self.TOK_SUBTYPE_START) - ) or ( - (tokens.next().ttype == self.TOK_TYPE_SUBEXPR) - and (tokens.next().tsubtype == self.TOK_SUBTYPE_START) - ) or ( - tokens.next().ttype == self.TOK_TYPE_OPERAND) - )): + elif not ( + ((tokens.next().ttype == self.TOK_TYPE_FUNCTION) and (tokens.next().tsubtype == self.TOK_SUBTYPE_START)) + or ((tokens.next().ttype == self.TOK_TYPE_SUBEXPR) and (tokens.next().tsubtype == self.TOK_SUBTYPE_START)) + or (tokens.next().ttype == self.TOK_TYPE_OPERAND) + ): pass else: - tokens2.add( - token.tvalue, self.TOK_TYPE_OP_IN, - self.TOK_SUBTYPE_INTERSECT) + tokens2.add(token.tvalue, self.TOK_TYPE_OP_IN, self.TOK_SUBTYPE_INTERSECT) continue tokens2.addRef(token) @@ -583,28 +560,16 @@ def EOF(): # switch infix "-" operator to prefix when appropriate, switch infix # "+" operator to noop when appropriate, identify operand and # infix-operator subtypes, pull "@" from in front of function names - while (tokens2.moveNext()): + while tokens2.moveNext(): token = tokens2.current() - if ( - (token.ttype == self.TOK_TYPE_OP_IN) - and (token.tvalue == "-") - ): - if (tokens2.BOF()): + if (token.ttype == self.TOK_TYPE_OP_IN) and (token.tvalue == "-"): + if tokens2.BOF(): token.ttype = self.TOK_TYPE_OP_PRE elif ( - ( - (tokens2.previous().ttype == self.TOK_TYPE_FUNCTION) - and (tokens2.previous().tsubtype - == self.TOK_SUBTYPE_STOP) - ) or ( - (tokens2.previous().ttype == self.TOK_TYPE_SUBEXPR) - and (tokens2.previous().tsubtype - == self.TOK_SUBTYPE_STOP) - ) or ( - tokens2.previous().ttype == self.TOK_TYPE_OP_POST - ) or ( - tokens2.previous().ttype == self.TOK_TYPE_OPERAND - ) + ((tokens2.previous().ttype == self.TOK_TYPE_FUNCTION) and (tokens2.previous().tsubtype == self.TOK_SUBTYPE_STOP)) + or ((tokens2.previous().ttype == self.TOK_TYPE_SUBEXPR) and (tokens2.previous().tsubtype == self.TOK_SUBTYPE_STOP)) + or (tokens2.previous().ttype == self.TOK_TYPE_OP_POST) + or (tokens2.previous().ttype == self.TOK_TYPE_OPERAND) ): token.tsubtype = self.TOK_SUBTYPE_MATH @@ -613,26 +578,14 @@ def EOF(): continue - if ( - (token.ttype == self.TOK_TYPE_OP_IN) - and (token.tvalue == "+") - ): + if (token.ttype == self.TOK_TYPE_OP_IN) and (token.tvalue == "+"): if tokens2.BOF(): token.ttype = self.TOK_TYPE_NOOP elif ( - ( - (tokens2.previous().ttype == self.TOK_TYPE_FUNCTION) - and (tokens2.previous().tsubtype - == self.TOK_SUBTYPE_STOP) - ) or ( - (tokens2.previous().ttype == self.TOK_TYPE_SUBEXPR) - and (tokens2.previous().tsubtype - == self.TOK_SUBTYPE_STOP) - ) or ( - tokens2.previous().ttype == self.TOK_TYPE_OP_POST - ) or ( - tokens2.previous().ttype == self.TOK_TYPE_OPERAND - ) + ((tokens2.previous().ttype == self.TOK_TYPE_FUNCTION) and (tokens2.previous().tsubtype == self.TOK_SUBTYPE_STOP)) + or ((tokens2.previous().ttype == self.TOK_TYPE_SUBEXPR) and (tokens2.previous().tsubtype == self.TOK_SUBTYPE_STOP)) + or (tokens2.previous().ttype == self.TOK_TYPE_OP_POST) + or (tokens2.previous().ttype == self.TOK_TYPE_OPERAND) ): token.tsubtype = self.TOK_SUBTYPE_MATH @@ -641,12 +594,11 @@ def EOF(): continue - if ((token.ttype == self.TOK_TYPE_OP_IN) - and (len(token.tsubtype) == 0)): - if (("<>=").find(token.tvalue[0:1]) != -1): + if (token.ttype == self.TOK_TYPE_OP_IN) and (len(token.tsubtype) == 0): + if ("<>=").find(token.tvalue[0:1]) != -1: token.tsubtype = self.TOK_SUBTYPE_LOGICAL - elif (token.tvalue == "&"): + elif token.tvalue == "&": token.tsubtype = self.TOK_SUBTYPE_CONCAT else: @@ -654,13 +606,12 @@ def EOF(): continue - if ((token.ttype == self.TOK_TYPE_OPERAND) - and (len(token.tsubtype) == 0)): + if (token.ttype == self.TOK_TYPE_OPERAND) and (len(token.tsubtype) == 0): try: float(token.tvalue) except ValueError: - if ((token.tvalue == 'TRUE') or (token.tvalue == 'FALSE')): + if (token.tvalue == "TRUE") or (token.tvalue == "FALSE"): token.tsubtype = self.TOK_SUBTYPE_LOGICAL else: token.tsubtype = self.TOK_SUBTYPE_RANGE @@ -669,8 +620,8 @@ def EOF(): continue - if (token.ttype == self.TOK_TYPE_FUNCTION): - if (token.tvalue[0:1] == "@"): + if token.ttype == self.TOK_TYPE_FUNCTION: + if token.tvalue[0:1] == "@": token.tvalue = token.tvalue[1:] continue @@ -679,13 +630,13 @@ def EOF(): # move all tokens to a new collection, excluding all noops tokens = f_tokens() - while (tokens2.moveNext()): - if (tokens2.current().ttype != self.TOK_TYPE_NOOP): + while tokens2.moveNext(): + if tokens2.current().ttype != self.TOK_TYPE_NOOP: tokens.addRef(tokens2.current()) tokens.reset() return tokens - def parse(self, formula): + def parse(self, formula: str) -> f_tokens: self.tokens = self.getTokens(formula) return self.tokens diff --git a/xlcalculator/utils.py b/xlcalculator/utils.py index 71da5f3..ef9054a 100644 --- a/xlcalculator/utils.py +++ b/xlcalculator/utils.py @@ -7,7 +7,7 @@ MAX_ROW = 1048576 -def resolve_sheet(sheet_str): +def resolve_sheet(sheet_str: str) -> str: sheet_str = sheet_str.strip() sheet_match = re.match(SHEET_TITLE.strip(), sheet_str + '!') if sheet_match is None: @@ -18,7 +18,7 @@ def resolve_sheet(sheet_str): return sheet_match.group("quoted") or sheet_match.group("notquoted") -def resolve_address(addr): +def resolve_address(addr: str) -> tuple[str, str, str]: # Addresses without sheet name are not supported. sheet_str, addr_str = addr.split('!') sheet = resolve_sheet(sheet_str) @@ -27,7 +27,8 @@ def resolve_address(addr): return sheet, col, row -def resolve_ranges(ranges, default_sheet='Sheet1'): +# TODO: double check the return type, that seems odd +def resolve_ranges(ranges: str, default_sheet: str='Sheet1') -> tuple[str, list[list[str]]]: # noqa: E252 sheet = None range_cells = collections.defaultdict(set) for rng in ranges.split(','): diff --git a/xlcalculator/xlfunctions/func_xltypes.py b/xlcalculator/xlfunctions/func_xltypes.py index a402903..59cdd27 100644 --- a/xlcalculator/xlfunctions/func_xltypes.py +++ b/xlcalculator/xlfunctions/func_xltypes.py @@ -448,13 +448,13 @@ def flatten(self, xltype=None, filt=None): return list(filter(filt, [cast(item) for item in self.values.flat])) def cast_to_numbers(self): - return self.applymap(_safe_cast(Number.cast, Number(0.0))) + return self.map(_safe_cast(Number.cast, Number(0.0))) def cast_to_booleans(self): - return self.applymap(_safe_cast(Boolean.cast, Boolean(True))) + return self.map(_safe_cast(Boolean.cast, Boolean(True))) def cast_to_texts(self): - return self.applymap(_safe_cast(Text.cast, Text(''))) + return self.map(_safe_cast(Text.cast, Text(''))) class Expr: diff --git a/xlcalculator/xlfunctions/utils.py b/xlcalculator/xlfunctions/utils.py index aaa15da..e504c0c 100644 --- a/xlcalculator/xlfunctions/utils.py +++ b/xlcalculator/xlfunctions/utils.py @@ -1,16 +1,18 @@ import datetime +from typing import Union EXCEL_EPOCH = datetime.datetime(1900, 1, 1) -def number_to_datetime(value): +def number_to_datetime(value: Union[int, float]) -> datetime.datetime: offset = 2 if value > 58 else 1 delta = datetime.timedelta( - days=int(value) - offset, seconds=(value % 1) * 24 * 60 * 60) + days=int(value) - offset, seconds=(value % 1) * 24 * 60 * 60 + ) return EXCEL_EPOCH + delta -def datetime_to_number(value): +def datetime_to_number(value: datetime.datetime) -> float: delta = value - EXCEL_EPOCH # Excel treats 1900 as a leap year. offset = 2 if delta.days > 58 else 1 diff --git a/xlcalculator/xlfunctions/xl.py b/xlcalculator/xlfunctions/xl.py index a37311d..694c323 100644 --- a/xlcalculator/xlfunctions/xl.py +++ b/xlcalculator/xlfunctions/xl.py @@ -1,6 +1,7 @@ import functools import inspect import typing +from typing import Union from . import func_xltypes, xlerrors @@ -20,7 +21,7 @@ class Functions(dict): - def register(self, func, name=None): + def register(self, func, name: Union[str, None] = None): if name is None: name = func.__name__ self[name] = func diff --git a/xlcalculator/xltypes.py b/xlcalculator/xltypes.py index 08ed809..835b1a2 100644 --- a/xlcalculator/xltypes.py +++ b/xlcalculator/xltypes.py @@ -31,11 +31,9 @@ def __post_init__(self): """Supplimentary initialisation.""" self.tokens = tokenizer.ExcelParser().getTokens(self.formula).items for token in self.tokens: - if ( - (token.ttype == ExcelParserTokens.TOK_TYPE_OPERAND) - and (token.tsubtype == ExcelParserTokens.TOK_SUBTYPE_RANGE) - and (token.tvalue not in self.terms) - ): + if (token.ttype == ExcelParserTokens.TOK_TYPE_OPERAND + and token.tsubtype == ExcelParserTokens.TOK_SUBTYPE_RANGE + and token.tvalue not in self.terms): # Make sure we have a full address. term = token.tvalue if '!' not in term: