Repository: dynaconf/dynaconf (python)

Issue:
[bug] negative numbers not parsed as integers from environment variables
**Describe the bug**
In version `1.3.9` environment variables that are negative represented as strings (e.g. `DYNACONF_NUM="-1"`) are not cast to type `int` when loaded in Python. This isn't true in  version `1.3.4`.

**To Reproduce**
Steps to reproduce the behavior:

1. Having the following folder structure

<!-- Describe or use the command `$ tree -v` and paste below -->

<details>
<summary> Project structure </summary>

```bash
.
├── test.py
└── settings.toml
```
</details>

2. Having the following config files:

<!-- Please adjust if you are using different files and formats! -->

<details>
<summary> Config files </summary>

**./settings.toml**
```toml
num = 1
```

</details>

3. Having the following app code:

<details>
<summary> Code </summary>

**./test.py**
```python
from dynaconf import Dynaconf

settings = Dynaconf(settings_file="./settings.toml")
print(settings.num)
print(type(settings.num))
```

</details>

4. Executing under the following environment

<details>
<summary> Execution </summary>

```bash
$ export DYNACONF_NUM="-1"
$ python ./test.py
```

</details>

**Expected behavior**
The python script should print the following:
```bash
-1
<class 'int'>
```

**Actual behavior**
The python script prints the following:
```bash
-1
<class 'str'>
```

**Environment (please complete the following information):**
 - OS: macOS Monerey 12.5.1
 - Dynaconf Version [e.g. 3.1.9]

**Additional context**
Again, this works as expected in version `3.1.4` so I assume something has changed in a recent version.


Pull request description:
Parse negative numbers from envvar Fix #799 and Fix #585
TOML reads signed integers as int on Python

```toml
num = -1
```
turns to
```py
assert settings.num == -1
assert isinstance(settings.num, int)
```

On validators, when `is_type_of=str` is passed we need to bypass this conversion and force a `str`


Patch:
diff --git a/Makefile b/Makefile
index 7e2859c..3bac297 100644
--- a/Makefile
+++ b/Makefile
@@ -133,6 +133,7 @@ test_examples:
 	cd example/issues/741_envvars_ignored;pwd;sh recreate.sh
 	cd example/issues/705_flask_dynaconf_init;pwd;make test;make clean
 	cd example/issues/794_includes;pwd;python app.py
+	cd example/issues/799_negative_numbers;pwd;DYNACONF_NUM="-1" python app.py
 
 test_vault:
 	# @cd example/vault;pwd;python write.py
diff --git a/dynaconf/utils/parse_conf.py b/dynaconf/utils/parse_conf.py
index c6da557..2fcf723 100644
--- a/dynaconf/utils/parse_conf.py
+++ b/dynaconf/utils/parse_conf.py
@@ -356,7 +356,6 @@ def parse_conf_data(data, tomlfy=False, box_settings=None):
     box_settings = box_settings or {}
 
     if isinstance(data, (tuple, list)):
-
         # recursively parse each sequence item
         return [
             parse_conf_data(item, tomlfy=tomlfy, box_settings=box_settings)
@@ -372,13 +371,6 @@ def parse_conf_data(data, tomlfy=False, box_settings=None):
             )
         return _parsed
 
-    if (
-        isinstance(data, str)
-        and data.startswith(("+", "-"))
-        and data[1:].isdigit()
-    ):
-        return data
-
     # return parsed string value
     return _parse_conf_data(data, tomlfy=tomlfy, box_settings=box_settings)
 
diff --git a/dynaconf/validator.py b/dynaconf/validator.py
index f3c581d..60dbfde 100644
--- a/dynaconf/validator.py
+++ b/dynaconf/validator.py
@@ -135,6 +135,9 @@ class Validator:
         self.envs: Sequence[str] | None = None
         self.apply_default_on_none = apply_default_on_none
 
+        # See #585
+        self.is_type_of = operations.get("is_type_of")
+
         if isinstance(env, str):
             self.envs = [env]
         elif isinstance(env, (list, tuple)):
@@ -243,6 +246,20 @@ class Validator:
             else:
                 default_value = empty
 
+            # THIS IS A FIX FOR #585 in contrast with #799
+            # toml considers signed strings "+-1" as integers
+            # however existing users are passing strings
+            # to default on validator (see #585)
+            # The solution we added on #667 introduced a new problem
+            # This fix here makes it to work for both cases.
+            if (
+                isinstance(default_value, str)
+                and default_value.startswith(("+", "-"))
+                and self.is_type_of is str
+            ):
+                # avoid TOML from parsing "+-1" as integer
+                default_value = f"'{default_value}'"
+
             value = self.cast(
                 settings.setdefault(
                     name,
diff --git a/example/issues/799_negative_numbers/app.py b/example/issues/799_negative_numbers/app.py
new file mode 100644
index 0000000..f240b46
--- /dev/null
+++ b/example/issues/799_negative_numbers/app.py
@@ -0,0 +1,11 @@
+from __future__ import annotations
+
+from dynaconf import Dynaconf
+
+settings = Dynaconf(settings_files=["settings.toml"])
+
+print(settings.num)
+print(type(settings.num))
+
+assert settings.num == -1, settings.num
+assert isinstance(settings.num, int)
diff --git a/example/issues/799_negative_numbers/settings.toml b/example/issues/799_negative_numbers/settings.toml
new file mode 100644
index 0000000..e32e0e0
--- /dev/null
+++ b/example/issues/799_negative_numbers/settings.toml
@@ -0,0 +1,1 @@
+num = -1


Test patch:
diff --git a/tests/test_env_loader.py b/tests/test_env_loader.py
index 2d99ea4..2e85a00 100644
--- a/tests/test_env_loader.py
+++ b/tests/test_env_loader.py
@@ -206,6 +206,20 @@ def test_backwards_compat_using_env_argument():
     assert settings.VALUE == "BLARG as prefix"
 
 
+def test_load_signed_integer():
+    environ["799_SIGNED_NEG_INT"] = "-1"
+    environ["799_SIGNED_POS_INT"] = "+1"
+    load_from_env(
+        identifier="env_global",
+        key=None,
+        prefix="799",
+        obj=settings,
+        silent=True,
+    )
+    assert settings.SIGNED_NEG_INT == -1
+    assert settings.SIGNED_POS_INT == 1
+
+
 def test_env_is_not_str_raises():
     with pytest.raises(TypeError):
         load_from_env(settings, prefix=int)
diff --git a/tests/test_validators.py b/tests/test_validators.py
index bf67a92..71eaa1f 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -739,6 +739,17 @@ def test_toml_should_not_change_validator_type_with_is_type_set():
     assert settings.test == "+172800"
 
 
+def test_toml_should_not_change_validator_type_with_is_type_not_set_int():
+    settings = Dynaconf(
+        validators=[Validator("TEST", default="+172800")]
+        # The ways to force a string is
+        # passing is_type_of=str
+        # or default="@str +172800" or default="'+172800'"
+    )
+
+    assert settings.test == +172800
+
+
 def test_toml_should_not_change_validator_type_using_at_sign():
     settings = Dynaconf(
         validators=[Validator("TEST", is_type_of=str, default="@str +172800")]