diff --git a/unittests/test_sanity_functions.py b/unittests/test_sanity_functions.py
index 7e6368938..0e9367027 100644
--- a/unittests/test_sanity_functions.py
+++ b/unittests/test_sanity_functions.py
@@ -473,6 +473,18 @@ def test_assert_reference():
                              r'\(l=-1\.2, u=-0\.9\)'):
         sn.evaluate(sn.assert_reference(-0.8, -1, -0.2, 0.1))

+    # Check that bounds are correctly calculated in case that lower bound
+    # reaches zero (see also GH issue #3430)
+    with pytest.raises(SanityError,
+                       match=r'1 is beyond reference value 0\.1 '
+                             r'\(l=0\.0, u=0\.1\)'):
+        assert sn.assert_reference(1, 0.1, -1.0, 0)
+
+    with pytest.raises(SanityError,
+                       match=r'-1 is beyond reference value -0\.1 '
+                             r'\(l=-0\.1, u=-0\.0\)'):
+        assert sn.assert_reference(-1, -0.1, 0, 1.0)
+
     # Check invalid thresholds
     with pytest.raises(SanityError,
                        match=r'invalid high threshold value: -0\.1'):

Performance threshold goes to -inf when it should be zero.

In attempting to create a performance test where zero is the correct value, I created the following reference (since a value of zero results in no reference check being performed; see https://github.com/reframe-hpc/reframe/issues/2857):

```
        self.reference = {
            '*': {
                'Gflops': (None, None, None, 'Gflops'),
                'Exponent': (None, None, None, '10exp'),
                'Time': (None, None, None, 'seconds'),
                'failed_tests': (.1, -1.0, 0, 'tests'),
                'skipped_tests': (.1, -1.0, 0, 'tests')
        }}
```
I was thinking for the failed and skipped tests, this would create a lower bound of zero and upper bound of .1, allowing zero to pass, but integers larger than that would fail.
This works for me in that values of zero pass, and 1 or greater values fail, but what is unexpected is that the lower bound gets set to -inf:

```
  * Reason: performance error: failed to meet references: skipped_tests=1 tests, expected 0.1 (l=-inf, u=0.1)
```
As an additional point of information when I set the lower bound values to -2.0 (or anything greater than 1 abs. value, to see what would happen), the following was printed:
```
	invalid low threshold value: -2.0
	invalid low threshold value: -2.0
```
But when I choose 2.0 as an upper bound, that seems to be fine:
```
                'failed_tests': (.1, -1.0, 2.0, 'tests'),
                'skipped_tests': (.1, -1.0, 2.0, 'tests')
```
results in the range being interpretted as follows:
```
  * Reason: performance error: failed to meet references: skipped_tests=1 tests, expected 0.1 (l=-inf, u=0.30000000000000004)
```
It seems to me that the treatment of lower and upper threshold values should be consistent, and this does not seem consistent.
Although when I switch the reference value to a negative, that reverses the issue:
```
                'failed_tests': (-1.0, 0, 2.0, 'tests'),
                'skipped_tests': (-1.0, 0, 2.0, 'tests')
```
results in:
```
	invalid high threshold value: 2.0
	invalid high threshold value: 2.0
```
and to confirm, I set the lower threshold to -2.0 with a negative reference value to check:
```
                'failed_tests': (-1.0, -2.0, 1.0, 'tests'),
                'skipped_tests': (-1.0, -2.0, 1.0, 'tests')
```
results in:
```
PASSED
```
Meaning there was no error with the -2.0 reference value in this case. I am not sure what value the upper bound was getting set to in this case, since it wasn't printed out, but since there was no error, it must not be getting set to zero. If it matches the pattern, I am thinking it might be set to inf.
I am thinking there is something odd that happens when the threshold range crosses zero or has one end that is zero.

pip install -e .;
pip install pytest pytest-json-report;

pytest --json-report --json-report-file=report_pytest.json unittests/test_sanity_functions.py

diff --git a/reframe/utility/sanity.py b/reframe/utility/sanity.py
index a4f57a301..1228586ff 100644
--- a/reframe/utility/sanity.py
+++ b/reframe/utility/sanity.py
@@ -8,6 +8,7 @@
 import contextlib
 import glob as pyglob
 import itertools
+import math
 import os
 import re
 import sys
@@ -576,8 +577,14 @@ def calc_bound(thres):

         return ref*(1 + thres)

-    lower = calc_bound(lower_thres) or float('-inf')
-    upper = calc_bound(upper_thres) or float('inf')
+    lower = calc_bound(lower_thres)
+    if lower is None:
+        lower = -math.inf
+
+    upper = calc_bound(upper_thres)
+    if upper is None:
+        upper = math.inf
+
     try:
         evaluate(assert_bounded(val, lower, upper))
     except SanityError:


diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index 68c38e95..23990820 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -1744,6 +1744,13 @@ def test_assign_expr(self):
         print(x)
         ''')

+    def test_assign_expr_after_annotation(self):
+        self.flakes("""
+        a: int
+        print(a := 3)
+        print(a)
+        """)
+
     def test_assign_expr_generator_scope(self):
         """Test assignment expressions in generator expressions."""
         self.flakes('''

Walrus operator + annotation can cause F821 in version 3.3.0

In order to type hint a variable used in a walrus expression, I put the type hint outside of the expression like in the following example (test.py):

```
a: int
if a := 3:
    print(a)

print(a)
```

Variable "a" is defined in both places

```
$ python3 test.py
3
3
```

In pyflakes version 3.2.0 this produced no errors

```pip install flake8==7.1.2 pyflakes==3.2.0```

```
$ flake8 test.py
```

In pyflakes version 3.3.0 this produces F821 even though "a" is defined

```pip install flake8==7.2.0 pyflakes==3.3.0```

```
$ flake8 test.py
test.py:3:11: F821 undefined name 'a'
test.py:5:7: F821 undefined name 'a'
```

I am on Windows with Python version 3.12.8

pytest --json-report --json-report-file=report_pytest.json pyflakes/test/test_other.py

diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index c7d3e882..9888d7ac 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -1008,8 +1008,12 @@ def addBinding(self, node, value):
                     for scope in reversed(self.scopeStack)
                     if not isinstance(scope, GeneratorScope)
                 )
-                # it may be a re-assignment to an already existing name
-                scope.setdefault(value.name, value)
+                if value.name in scope and isinstance(scope[value.name], Annotation):
+                    # re-assignment to name that was previously only an annotation
+                    scope[value.name] = value
+                else:
+                    # it may be a re-assignment to an already existing name
+                    scope.setdefault(value.name, value)
             else:
                 self.scope[value.name] = value


diff --git a/tests/dialects/test_mysql.py b/tests/dialects/test_mysql.py
index 06638f16c5..4a28176ccf 100644
--- a/tests/dialects/test_mysql.py
+++ b/tests/dialects/test_mysql.py
@@ -83,6 +83,12 @@ def test_ddl(self):
         self.validate_identity(
             "CREATE OR REPLACE VIEW my_view AS SELECT column1 AS `boo`, column2 AS `foo` FROM my_table WHERE column3 = 'some_value' UNION SELECT q.* FROM fruits_table, JSON_TABLE(Fruits, '$[*]' COLUMNS(id VARCHAR(255) PATH '$.$id', value VARCHAR(255) PATH '$.value')) AS q",
         )
+        self.validate_identity(
+            "CREATE TABLE test_table (id INT AUTO_INCREMENT, PRIMARY KEY (id) USING BTREE)"
+        )
+        self.validate_identity(
+            "CREATE TABLE test_table (id INT AUTO_INCREMENT, PRIMARY KEY (id) USING HASH)"
+        )
         self.validate_identity(
             "/*left*/ EXPLAIN SELECT /*hint*/ col FROM t1 /*right*/",
             "/* left */ DESCRIBE /* hint */ SELECT col FROM t1 /* right */",

MySQL dialect fails to parse PRIMARY KEY USING BTREE syntax

**Before you file an issue**
- Make sure you specify the "read" dialect eg. `parse_one(sql, read="spark")`
- Make sure you specify the "write" dialect eg. `ast.sql(dialect="duckdb")`
- Check if the issue still exists on main
yes, the issue still exists. I'v tried the latest version.
**Fully reproducible code snippet**
Please include a fully reproducible code snippet or the input sql, dialect, and expected output.
mysql:
`CREATE TABLE test_table (
	id INT AUTO_INCREMENT,
	alarm_category VARCHAR ( 255 ),
	alarm_rule VARCHAR ( 255 ),
PRIMARY KEY(id) USING BTREE
)`

**Official Documentation**
Please include links to official SQL documentation related to your issue.
https://sqlglot.com/sqlglot.html#parse_one

reproduce
```python
sql = "CREATE TABLE test_table ( id INT AUTO_INCREMENT, alarm_category VARCHAR ( 255 ), alarm_rule VARCHAR ( 255 ), PRIMARY KEY(id) USING BTREE )"
sqlglot.parse_one(sql,read='mysql')

# result

# sqlglot.errors.ParseError: Expecting ). Line 1, Col: 130.
#   id INT AUTO_INCREMENT, alarm_category VARCHAR ( 255 ), alarm_rule VARCHAR ( 255 ), PRIMARY KEY(id) USING BTREE )
```
this is the reproducible python code

pytest --json-report --json-report-file=report_pytest.json tests/dialects/test_mysql.py

diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 24bf1492f7..f5ed3a8923 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1291,6 +1291,10 @@ class Parser(metaclass=_Parser):
             "SIMPLE",
         ),
         "INITIALLY": ("DEFERRED", "IMMEDIATE"),
+        "USING": (
+            "BTREE",
+            "HASH",
+        ),
         **dict.fromkeys(("DEFERRABLE", "NORELY"), tuple()),
     }


diff --git a/tests/importing/test_load_from_msp.py b/tests/importing/test_load_from_msp.py
index 891ac002..839b604c 100644
--- a/tests/importing/test_load_from_msp.py
+++ b/tests/importing/test_load_from_msp.py
@@ -2,7 +2,7 @@ import os
 import numpy as np
 import pytest
 from matchms import Spectrum
-from matchms.importing.load_from_msp import load_from_msp, parse_metadata
+from matchms.importing.load_from_msp import get_peak_values, load_from_msp, parse_metadata
 from tests.builder_Spectrum import SpectrumBuilder

@@ -285,3 +285,13 @@ def test_parse_metadata(input_line, expected_output):
     params = {}
     parse_metadata(input_line, params)
     assert params == expected_output
+
+
+@pytest.mark.parametrize("line, expected", [
+ ["496	7.1E-05", ([496.0], [7.1E-05])],
+ ["496	7.1e-05", ([496.0], [7.1e-05])],
+ ["85:201 86:55 87:10 88:4 89:315", ([85.0, 86.0, 87.0, 88.0, 89.0], [201.0, 55.0, 10.0, 4.0, 315.0])],
+])
+def test_get_peak_values(line, expected):
+    actual = get_peak_values(line)
+    assert actual == expected
\ No newline at end of file

tests/importing/test_load_from_msp.py::test_get_peak_values[85:201 86:55 87:10 88:4 89:315-expected2]

pytest --json-report --json-report-file=report_pytest.json tests/importing/test_load_from_msp.py

diff --git a/matchms/importing/load_from_msp.py b/matchms/importing/load_from_msp.py
index cf7726a4..47dbedcc 100644
--- a/matchms/importing/load_from_msp.py
+++ b/matchms/importing/load_from_msp.py
@@ -109,7 +109,7 @@ def _parse_line_with_peaks(rline: str) -> Tuple[List[float], List[float], str]:

 def get_peak_values(peak: str) -> Tuple[List[float], List[float]]:
     """Get the m/z and intensity value from the line containing the peak information."""
-    tokens = re.findall(r"(\d+(?:\.\d+)?(?:e[-+]?\d+)?)", peak)
+    tokens = re.findall(r"(\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)", peak)
     if len(tokens) % 2 != 0:
         raise RuntimeError("Wrong peak format detected!")


diff --git a/tests/analyzer/metadata/test_bundled_binary.py b/tests/analyzer/metadata/test_bundled_binary.py
index 07f1845..7a01283 100644
--- a/tests/analyzer/metadata/test_bundled_binary.py
+++ b/tests/analyzer/metadata/test_bundled_binary.py
@@ -19,6 +19,8 @@ class TestBundleBinary:
     binary_sample_elf = (
         b"\x7F\x45\x4C\x46" + b"0x90" * 10
     )  # elf magic number plus nop sled
+    binary_sample_macho32 = b"\xFE\xED\xFA\xCE" + b"0x90" * 10
+    binary_sample_macho64 = b"\xFE\xED\xFA\xCF" + b"0x90" * 10

     @pytest.mark.parametrize(
         "detector",
@@ -52,6 +54,38 @@ class TestBundleBinary:
             matches, _ = detector.detect({}, dir)
             assert matches

+    @pytest.mark.parametrize(
+        "detector",
+        [
+            (pypi_detector),
+            (npm_detector),
+        ],
+    )
+    def test_macho32(self, detector: BundledBinary):
+        with tempfile.TemporaryDirectory() as dir:
+            full_path = os.path.join(dir, "package")
+            os.mkdir(full_path)
+            with open(os.path.join(full_path, "linux.txt"), "wb") as f:
+                f.write(self.binary_sample_macho32)
+            matches, _ = detector.detect({}, dir)
+            assert matches
+
+    @pytest.mark.parametrize(
+        "detector",
+        [
+            (pypi_detector),
+            (npm_detector),
+        ],
+    )
+    def test_macho64(self, detector: BundledBinary):
+        with tempfile.TemporaryDirectory() as dir:
+            full_path = os.path.join(dir, "package")
+            os.mkdir(full_path)
+            with open(os.path.join(full_path, "linux.txt"), "wb") as f:
+                f.write(self.binary_sample_macho64)
+            matches, _ = detector.detect({}, dir)
+            assert matches
+
     @pytest.mark.parametrize(
         "detector",
         [

Add Mach-O magic bytes to the bundled binary detector
The [bundled binary detector](https://github.com/DataDog/guarddog/blob/v2.1.0/guarddog/analyzer/metadata/bundled_binary.py#L17) currently detects **ELF** (Unix) and **exe** (Windows) files, but it does not account for the magic bytes for **Mach-O** (Mac) files. This could lead to malicious packages targeting Macs to go unnoticed.

For reference:

![Image](https://github.com/user-attachments/assets/7369b953-b3ea-4d0f-a536-70d84d2056ab)

See https://en.wikipedia.org/wiki/Mach-O#Mach-O_header for more info.

pytest --json-report --json-report-file=report_pytest.json tests/analyzer/metadata/test_bundled_binary.py

diff --git a/guarddog/analyzer/metadata/bundled_binary.py b/guarddog/analyzer/metadata/bundled_binary.py
index 5b6e875..81ed040 100644
--- a/guarddog/analyzer/metadata/bundled_binary.py
+++ b/guarddog/analyzer/metadata/bundled_binary.py
@@ -14,7 +14,12 @@ class BundledBinary(Detector):

     # magic bytes are the first few bytes of a file that can be used to identify the file type
     # regardless of their extension
-    magic_bytes = {"exe": b"\x4D\x5A", "elf": b"\x7F\x45\x4C\x46"}
+    magic_bytes = {
+        "exe": b"\x4D\x5A",
+        "elf": b"\x7F\x45\x4C\x46",
+        "macho32": b"\xFE\xED\xFA\xCE",
+        "macho64": b"\xFE\xED\xFA\xCF",
+    }

     def __init__(self):
         super().__init__(


diff --git a/test/test_render_helpers.py b/test/test_render_helpers.py
index c17740f..4279725 100644
--- a/test/test_render_helpers.py
+++ b/test/test_render_helpers.py
@@ -110,6 +110,22 @@ def test_markdown_toc():
     assert to_html("#foo\n#bar").toc_html  # type: ignore

+def test_mixed_toc():
+    """
+    markdown2 can handle mixed markdown and HTML headings.
+
+    Let's test that this works as expected.
+    """
+    expected = [
+        "
",
+        '
foo
',
+        '
bar
',
+        "
",
+        "",
+    ]
+    assert to_html("#foo\n
bar
").toc_html == "\n".join(expected)
+
+
 @pytest.mark.parametrize(
     "md,html",
     [

Include HTML headers in ToC
#### Problem Description

When a docstring includes HTML headers (`<h1>`, etc.) for example when I used an `.. include:: some_file.html` (or some markdown file which mixes markdown and HTML headers for some reason) directive, those headers are not included in the table of contents.

#### Proposal
markdown2 already supports this feature: https://github.com/trentm/python-markdown2/pull/538.
All one would need to do, is set `header-ids["mixed"] = True` for the header-ids extra, which pdoc already uses.

#### Additional Context
Essentially, I would consider this the reverse of the `markdown-in-html` setting, which is already supported by pdoc. So I do not see a reason not to support this.

test/test_render_helpers.py::test_markdown_autolink[See https://www.python.org.-
See https://www.python.org.
\n]

test/test_render_helpers.py::test_markdown_autolink[See **https://www.python.org**.-
See https://www.python.org.
\n]

test/test_render_helpers.py::test_markdown_autolink[See the [Python home page ](https://www.python.org) for info.-
See the Python home page  for info.
\n]

pytest --json-report --json-report-file=report_pytest.json test/test_render_helpers.py

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 747d22f..3337d33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@

 ## Unreleased: pdoc next

+- Include included HTML headers in the ToC by default by enabling markdown2's `mixed=True` option of the `header-ids` extra
+  (#806, @mrossinek)

 ## 2025-04-21: pdoc 15.0.3

diff --git a/pdoc/render_helpers.py b/pdoc/render_helpers.py
index 5c1f339..c96194b 100644
--- a/pdoc/render_helpers.py
+++ b/pdoc/render_helpers.py
@@ -61,7 +61,7 @@ markdown_extensions = {
     "cuddled-lists": None,
     "fenced-code-blocks": {"cssclass": formatter.cssclass},
     "footnotes": None,
-    "header-ids": None,
+    "header-ids": {"mixed": True, "prefix": None, "reset-count": True},
     "link-patterns": None,
     "markdown-in-html": None,
     "mermaid": None,


diff --git a/qcelemental/tests/test_molecule.py b/qcelemental/tests/test_molecule.py
index 46d7ced..b2154c0 100644
--- a/qcelemental/tests/test_molecule.py
+++ b/qcelemental/tests/test_molecule.py
@@ -2,6 +2,7 @@
 Tests the imports and exports of the Molecule object.
 """

+
 import numpy as np
 import pytest

@@ -798,6 +799,15 @@ _ref_mol_multiplicity_hash = {
     "triplet": "7caca87a",
     "disinglet": "83a85546",
     "ditriplet": "71d6ba82",
+    # float mult
+    "singlet_point1": "4e9e2587",
+    "singlet_epsilon": "ad3f5fab",
+    "triplet_point1": "ad35cc28",
+    "triplet_point1_minus": "b63d6983",
+    "triplet_point00001": "7107b7ac",
+    "disinglet_epsilon": "fb0aaaca",
+    "ditriplet_point1": "33d47d5f",
+    "ditriplet_point00001": "7f0ac640",
 }

@@ -806,14 +816,26 @@ _ref_mol_multiplicity_hash = {
     [
         pytest.param(3, 3, False, "triplet"),
         pytest.param(3, 3, True, "triplet"),
-        # 3.1 -> 3 (validate=False) below documents the present bad behavior where a float mult
-        #   simply gets cast to int with no error. This will change soon. The validate=True throws a
-        #   nonspecific error that at least mentions type.
-        pytest.param(3.1, 3, False, "triplet"),
+        # before float multiplicity was allowed, 3.1 (below) was coerced into 3 with validate=False,
+        #   and validate=True threw a type-mentioning error. Now, 2.9 is allowed for both validate=T/F
+        pytest.param(3.1, 3.1, False, "triplet_point1"),
+        # validate=True counterpart fails b/c insufficient electrons in He for more than triplet
+        pytest.param(2.9, 2.9, False, "triplet_point1_minus"),
+        pytest.param(2.9, 2.9, True, "triplet_point1_minus"),
+        pytest.param(3.00001, 3.00001, False, "triplet_point00001"),
+        # validate=True counterpart fails like 3.1 above
+        pytest.param(2.99999, 2.99999, False, "triplet_point00001"),  # hash agrees w/3.00001 above b/c <CHARGE_NOISE
+        pytest.param(2.99999, 2.99999, True, "triplet_point00001"),
         pytest.param(3.0, 3, False, "triplet"),
         pytest.param(3.0, 3, True, "triplet"),
         pytest.param(1, 1, False, "singlet"),
         pytest.param(1, 1, True, "singlet"),
+        pytest.param(1.000000000000000000002, 1, False, "singlet"),
+        pytest.param(1.000000000000000000002, 1, True, "singlet"),
+        pytest.param(1.000000000000002, 1.000000000000002, False, "singlet_epsilon"),
+        pytest.param(1.000000000000002, 1.000000000000002, True, "singlet_epsilon"),
+        pytest.param(1.1, 1.1, False, "singlet_point1"),
+        pytest.param(1.1, 1.1, True, "singlet_point1"),
         pytest.param(None, 1, False, "singlet"),
         pytest.param(None, 1, True, "singlet"),
         # fmt: off
@@ -841,6 +863,9 @@ def test_mol_multiplicity_types(mult_in, mult_store, validate, exp_hash):
     [
         pytest.param(-3, False, "Multiplicity must be positive"),
         pytest.param(-3, True, "Multiplicity must be positive"),
+        pytest.param(0.9, False, "Multiplicity must be positive"),
+        pytest.param(0.9, True, "Multiplicity must be positive"),
+        pytest.param(3.1, True, "Inconsistent or unspecified chg/mult"),  # insufficient electrons in He
     ],
 )
 def test_mol_multiplicity_types_errors(mult_in, validate, error):
@@ -859,10 +884,11 @@ def test_mol_multiplicity_types_errors(mult_in, validate, error):
     [
         pytest.param(5, [3, 3], [3, 3], False, "ditriplet"),
         pytest.param(5, [3, 3], [3, 3], True, "ditriplet"),
-        # 3.1 -> 3 (validate=False) below documents the present bad behavior where a float mult
-        #   simply gets cast to int with no error. This will change soon. The validate=True throws a
-        #   irreconcilable error.
-        pytest.param(5, [3.1, 3.4], [3, 3], False, "ditriplet"),
+        # before float multiplicity was allowed, [3.1, 3.4] (below) were coerced into [3, 3] with validate=False.
+        #   Now, [2.9, 2.9] is allowed for both validate=T/F.
+        pytest.param(5, [3.1, 3.4], [3.1, 3.4], False, "ditriplet_point1"),
+        pytest.param(5, [2.99999, 3.00001], [2.99999, 3.00001], False, "ditriplet_point00001"),
+        pytest.param(5, [2.99999, 3.00001], [2.99999, 3.00001], True, "ditriplet_point00001"),
         # fmt: off
         pytest.param(5, [3.0, 3.], [3, 3], False, "ditriplet"),
         pytest.param(5, [3.0, 3.], [3, 3], True, "ditriplet"),
@@ -871,6 +897,18 @@ def test_mol_multiplicity_types_errors(mult_in, validate, error):
         pytest.param(1, [1, 1], [1, 1], True, "disinglet"),
         # None in frag_mult not allowed for validate=False
         pytest.param(1, [None, None], [1, 1], True, "disinglet"),
+        pytest.param(1, [1.000000000000000000002, 0.999999999999999999998], [1, 1], False, "disinglet"),
+        pytest.param(1, [1.000000000000000000002, 0.999999999999999999998], [1, 1], True, "disinglet"),
+        pytest.param(
+            1,
+            [1.000000000000002, 1.000000000000004],
+            [1.000000000000002, 1.000000000000004],
+            False,
+            "disinglet_epsilon",
+        ),
+        pytest.param(
+            1, [1.000000000000002, 1.000000000000004], [1.000000000000002, 1.000000000000004], True, "disinglet_epsilon"
+        ),
     ],
 )
 def test_frag_multiplicity_types(mol_mult_in, mult_in, mult_store, validate, exp_hash):
@@ -902,6 +940,9 @@ def test_frag_multiplicity_types(mol_mult_in, mult_in, mult_store, validate, exp
     [
         pytest.param([-3, 1], False, "Multiplicity must be positive"),
         pytest.param([-3, 1], True, "Multiplicity must be positive"),
+        pytest.param(
+            [3.1, 3.4], True, "Inconsistent or unspecified chg/mult"
+        ),  # insufficient e- for triplet+ on He in frag 1
     ],
 )
 def test_frag_multiplicity_types_errors(mult_in, validate, error):
diff --git a/qcelemental/tests/test_molparse_validate_and_fill_chgmult.py b/qcelemental/tests/test_molparse_validate_and_fill_chgmult.py
index 0c18649..0f85bb0 100644
--- a/qcelemental/tests/test_molparse_validate_and_fill_chgmult.py
+++ b/qcelemental/tests/test_molparse_validate_and_fill_chgmult.py
@@ -94,6 +94,12 @@ working_chgmults = [
         (-2.4, [-2.4, 0, 0], 3, [1, 2, 2]),
         "a83a3356",
     ),  # 166
+    (("He", None, [None], 2.8, [None]), (0, [0], 2.8, [2.8]), "3e10e7b5"),  # 180
+    (("He", None, [None], None, [2.8]), (0, [0], 2.8, [2.8]), "3e10e7b5"),  # 181
+    (("N/N/N", None, [None, None, None], 2.2, [2, 2, 2.2]), (0, [0, 0, 0], 2.2, [2, 2, 2.2]), "798ee5d4"),  # 183
+    (("N/N/N", None, [None, None, None], 4.2, [2, 2, 2.2]), (0, [0, 0, 0], 4.2, [2, 2, 2.2]), "ed6d1f35"),  # 185
+    (("N/N/N", None, [None, None, None], None, [2, 2, 2.2]), (0, [0, 0, 0], 4.2, [2, 2, 2.2]), "ed6d1f35"),  # 186
+    (("N/N/N", None, [2, -2, None], 2.2, [2, 2, 2.2]), (0, [2, -2, 0], 2.2, [2, 2, 2.2]), "66e655c0"),  # 187
 ]

@@ -153,6 +159,8 @@ def test_validate_and_fill_chgmult_mol_model(systemtranslator, inp, expected, ex
         ("Gh", None, [None], 3, [None]),  # 60
         ("Gh/He", None, [2, None], None, [None, None]),  # 62
         ("Gh/Ne", 2, [-2, None], None, [None, None]),  # 65b
+        ("He", None, [None], 3.2, [None]),  # 182
+        ("N/N/N", None, [None, None, None], 2.2, [None, None, 2.2]),  # 184
     ],
 )
 def test_validate_and_fill_chgmult_irreconcilable(systemtranslator, inp):
@@ -173,6 +181,8 @@ def test_validate_and_fill_chgmult_irreconcilable(systemtranslator, inp):
 # 35 - insufficient electrons
 # 55 - both (1, (1, 0.0, 0.0), 4, (1, 3, 2)) and (1, (0.0, 0.0, 1), 4, (2, 3, 1)) plausible
 # 65 - non-0/1 on Gh fragment errors normally but reset by zero_ghost_fragments
+# 182 - insufficient electrons on He
+# 184 - decline to guess fragment multiplicities when floats involved

 @pytest.fixture

Floating point number allowed for molecular charge but not molecular multiplicity
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
QCElemental allows floating point numbers for molecular charges, however when computing the spin with the fractional electron there is no way to represent the molecular multiplicity with just an integer.

**To Reproduce**
<!-- Code to reproduce the behavior: Please include the results of:-->
```python
import qcelemental as qcel
mol = qcel.models.Molecule(
    geometry=[0.0, 0.0, 0.5, 0.0, 0.0, -0.5],
    symbols=["H", "He"],
    molecular_charge=0.5,
    molecular_multiplicity=1.5,  # not possible
)
```

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
Consistent behavior for specifying molecular charge and molecular multiplicity.

**Additional context**
<!-- Add any other context about the problem here. -->

qcelemental/tests/test_molecule.py::test_molecular_weight[4He 0 0 0-args8-He-formula_dict8-4.00260325413-2-0.0]

qcelemental/tests/test_molecule.py::test_sparse_molecule_fields[He 0 0 0-None]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\n@He 0 0 5-args2-He2-formula_dict2-4.00260325413-2-0.0]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0-args0-He-formula_dict0-4.00260325413-2-0.0]

qcelemental/tests/test_molecule.py::test_sparse_molecule_fields[He 0 0 0\n--\n@He 0 0 5-extra_keys2]

qcelemental/tests/test_molecule.py::test_mol_multiplicity_types_errors[-3-False-Multiplicity must be positive]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\n@He 0 0 5-args3-He2-formula_dict3-4.00260325413-2-0.0]

qcelemental/tests/test_molecule.py::test_mol_multiplicity_types_errors[3.1-True-Inconsistent or unspecified chg/mult]

qcelemental/tests/test_molecule.py::test_frag_multiplicity_types_errors[mult_in1-True-Multiplicity must be positive]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\n@He 0 0 5-args5-He2-formula_dict5-8.00520650826-4-0.4233417684]

qcelemental/tests/test_molecule.py::test_sparse_molecule_fields[He@3.14 0 0 0-extra_keys4]

qcelemental/tests/test_molecule.py::test_sparse_molecule_fields[He4 0 0 0-extra_keys3]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\nHe 0 0 5-args1-He2-formula_dict1-8.00520650826-4-0.4233417684]

qcelemental/tests/test_molecule.py::test_sparse_molecule_fields[He 0 0 0\n--\nHe 0 0 5-extra_keys1]

qcelemental/tests/test_molecule.py::test_mol_multiplicity_types_errors[-3-True-Multiplicity must be positive]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\n@He 0 0 5-args7-He2-formula_dict7-4.00260325413-2-0.0]

qcelemental/tests/test_molecule.py::test_mol_multiplicity_types_errors[0.9-True-Multiplicity must be positive]

qcelemental/tests/test_molecule.py::test_frag_multiplicity_types_errors[mult_in0-False-Multiplicity must be positive]

qcelemental/tests/test_molecule.py::test_molecular_weight[5He4 0 0 0-args9-He-formula_dict9-5.012057-2-0.0]

qcelemental/tests/test_molecule.py::test_molecular_weight[He@3.14 0 0 0-args10-He-formula_dict10-3.14-2-0.0]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\n@He 0 0 5-args4-He2-formula_dict4-0.0-0-0.0]

qcelemental/tests/test_molecule.py::test_mol_multiplicity_types_errors[0.9-False-Multiplicity must be positive]

qcelemental/tests/test_molecule.py::test_molecular_weight[He 0 0 0\n--\n@He 0 0 5-args6-He2-formula_dict6-4.00260325413-2-0.0]

pytest --json-report --json-report-file=report_pytest.json qcelemental/tests/test_molecule.py qcelemental/tests/test_molparse_validate_and_fill_chgmult.py

diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index aa949f3..8aca4c9 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -17,7 +17,8 @@ jobs:
       matrix:
         python-version: ["3.7", "3.9", "3.11", "3.12"]
         pydantic-version: ["1", "2"]
-        runs-on: [ubuntu-latest, windows-latest]
+        # runs-on: [ubuntu-latest, windows-latest]
+        runs-on: [ubuntu-22.04, windows-latest]  # until drop py37
         exclude:
           - runs-on: windows-latest
             pydantic-version: "1"
diff --git a/.github/workflows/Lint.yml b/.github/workflows/Lint.yml
index c337352..c52ea9a 100644
--- a/.github/workflows/Lint.yml
+++ b/.github/workflows/Lint.yml
@@ -14,7 +14,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: "3.7"
+          python-version: "3.8"
       - name: Install black
         run: pip install "black>=22.1.0,<23.0a0"
       - name: Print code formatting with black (hints here if next step errors)
@@ -29,7 +29,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: "3.7"
+          python-version: "3.8"
       - name: Install poetry
         run: pip install poetry
       - name: Install repo
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 76edbc6..0cd1e4f 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -19,18 +19,19 @@ Changelog
 .. Misc.
 .. +++++

-- (:pr:`340`, :issue:`330`) Add molecular charge and multiplicity to Molecule repr formula,
-  so neutral singlet unchanged but radical cation has '2^formula+'.
-

 0.29.0 / 2024-MM-DD (Unreleased)
 --------------------------------

 Breaking Changes
 ++++++++++++++++
+- (:pr:`341`) `packaging` is now a required dependency.

 New Features
 ++++++++++++
+- (:pr:`350`, :pr:`318`, :issue:`317`) Make behavior consistent between molecular_charge/
+  fragment_charges and molecular_multiplicity/fragment_multiplicities by allowing floating point
+  numbers for multiplicities. @awvwgk
 - (:pr:`360`) ``Molecule`` learned new functions ``element_composition`` and ``molecular_weight``.
   The first gives a dictionary of element symbols and counts, while the second gives the weight in amu.
   Both can access the whole molecule or per-fragment like the existing ``nelectrons`` and
@@ -38,13 +39,30 @@ New Features

 Enhancements
 ++++++++++++
+- (:pr:`340`, :issue:`330`) Add molecular charge and multiplicity to Molecule repr formula,
+  so neutral singlet unchanged but radical cation has '2^formula+'. @awvwgk
+- (:pr:`341`) Use `packaging` instead of deprecated `setuptools` to provide version parsing for
+  `qcelemental.util.parse_version` and `qcelemental.util.safe_version`. This behaves slightly
+  different; "v7.0.0+N/A" was processed ok before but fails with newer version. @berquist
+- (:pr:`343`) Molecular and fragment multiplicities are now always enforced to be >=1.0. Previously
+  this wasn't checked for `Molecule(..., validate=False)`. Error messages will change sometimes
+  change for `validate=True` (run by default).
+- (:pr:`343`) `qcelemental.molparse` newly allows floats that are ints (e.g., 1.0) for multiplicity.
+  Previously it would raise an error about not being an int.
+- (:pr:`337`) Solidify the (unchanged) schema_name for `QCInputSpecification` and `AtomicResult`
+  into Literals where previously they had been regex strings coerced into a single name. The literals
+  allow pydantic to discriminate models, which benefits GeneralizedOptimizationInput/Result in
+  QCManyBody/QCEngine/OptKing. The only way this can interfere is if schema producers have whitespace
+  around `schema_name` for these models or if any `AtomicResult`s are still using "qc_schema_output",
+  which looks to have only been added for compatibility with pre-pydantic QCSchema.

 Bug Fixes
 +++++++++

 Misc.
 +++++
-- (:pr:`342`) Update some docs settings and requirements for newer tools.
+- (:pr:`344`, :issue:`282`) Add a citation file since QCElemental doesn't have a paper. @lilyminium
+- (:pr:`342`, :issue:`333`) Update some docs settings and requirements for newer tools.
 - (:pr:`353`) copied in pkg_resources.safe_version code as follow-up to Eric switch to packaging as both nwchem and gamess were now working.
 the try_harder_safe_version might be even bettter

diff --git a/qcelemental/models/molecule.py b/qcelemental/models/molecule.py
index 63070d5..215db12 100644
--- a/qcelemental/models/molecule.py
+++ b/qcelemental/models/molecule.py
@@ -185,7 +185,7 @@ class Molecule(ProtoModel):
         description="Additional comments for this molecule. Intended for pure human/user consumption and clarity.",
     )
     molecular_charge: float = Field(0.0, description="The net electrostatic charge of the molecule.")  # type: ignore
-    molecular_multiplicity: int = Field(1, description="The total multiplicity of the molecule.")  # type: ignore
+    molecular_multiplicity: float = Field(1, description="The total multiplicity of the molecule.")  # type: ignore

     # Atom data
     masses_: Optional[Array[float]] = Field(  # type: ignore
@@ -257,7 +257,7 @@ class Molecule(ProtoModel):
         "if not provided (and :attr:`~qcelemental.models.Molecule.fragments` are specified).",
         shape=["nfr"],
     )
-    fragment_multiplicities_: Optional[List[int]] = Field(  # type: ignore
+    fragment_multiplicities_: Optional[List[float]] = Field(  # type: ignore
         None,
         description="The multiplicity of each fragment in the :attr:`~qcelemental.models.Molecule.fragments` list. The index of this "
         "list matches the 0-index indices of :attr:`~qcelemental.models.Molecule.fragments` list. Will be filled in based on a set of "
@@ -421,12 +421,16 @@ class Molecule(ProtoModel):
             n = len(values["fragments_"])
             if len(v) != n:
                 raise ValueError("Fragment Multiplicities must be same number of entries as Fragments")
+        v = [(int(m) if m.is_integer() else m) for m in v]
         if any([m < 1.0 for m in v]):
             raise ValueError(f"Fragment Multiplicity must be positive: {v}")
         return v

     @validator("molecular_multiplicity")
     def _int_if_possible(cls, v, values, **kwargs):
+        if v.is_integer():
+            # preserve existing hashes
+            v = int(v)
         if v < 1.0:
             raise ValueError("Molecular Multiplicity must be positive")
         return v
@@ -502,7 +506,7 @@ class Molecule(ProtoModel):
         return fragment_charges

     @property
-    def fragment_multiplicities(self) -> List[int]:
+    def fragment_multiplicities(self) -> List[float]:
         fragment_multiplicities = self.__dict__.get("fragment_multiplicities_")
         if fragment_multiplicities is None:
             fragment_multiplicities = [self.molecular_multiplicity]
@@ -803,9 +807,7 @@ class Molecule(ProtoModel):
             data = getattr(self, field)
             if field == "geometry":
                 data = float_prep(data, GEOMETRY_NOISE)
-            elif field == "fragment_charges":
-                data = float_prep(data, CHARGE_NOISE)
-            elif field == "molecular_charge":
+            elif field in ["fragment_charges", "molecular_charge", "fragment_multiplicities", "molecular_multiplicity"]:
                 data = float_prep(data, CHARGE_NOISE)
             elif field == "masses":
                 data = float_prep(data, MASS_NOISE)
diff --git a/qcelemental/molparse/chgmult.py b/qcelemental/molparse/chgmult.py
index a311d08..493335c 100644
--- a/qcelemental/molparse/chgmult.py
+++ b/qcelemental/molparse/chgmult.py
@@ -19,7 +19,7 @@ def _high_spin_sum(mult_list):

 def _mult_ok(m):
-    return isinstance(m, (int, np.integer)) and m >= 1
+    return isinstance(m, (int, np.integer, float, np.float64)) and m >= 1

 def _sufficient_electrons_for_mult(z, c, m):
@@ -430,7 +430,16 @@ def validate_and_fill_chgmult(
     if molecular_multiplicity is None:  # unneeded, but shortens the exact lists
         frag_mult_hi = _high_spin_sum(_apply_default(fragment_multiplicities, 2))
         frag_mult_lo = _high_spin_sum(_apply_default(fragment_multiplicities, 1))
-        for m in range(frag_mult_lo, frag_mult_hi + 1):
+        try:
+            mult_range = range(frag_mult_lo, frag_mult_hi + 1)
+        except TypeError:
+            if frag_mult_lo == frag_mult_hi:
+                mult_range = [frag_mult_hi]
+            else:
+                raise ValidationError(
+                    f"Cannot process: please fully specify float multiplicity: m: {molecular_multiplicity} fm: {fragment_multiplicities}"
+                )
+        for m in mult_range:
             cgmp_exact_m.append(m)

     #   * (S6) suggest range of missing mult = tot - high_spin_sum(frag - 1),
@@ -450,7 +459,16 @@ def validate_and_fill_chgmult(

     for ifr in range(nfr):
         if fragment_multiplicities[ifr] is None:  # unneeded, but shortens the exact lists
-            for m in reversed(range(max(missing_mult_lo, 1), missing_mult_hi + 1)):
+            try:
+                mult_range = reversed(range(max(missing_mult_lo, 1), missing_mult_hi + 1))
+            except TypeError:
+                if missing_mult_lo == missing_mult_hi:
+                    mult_range = [missing_mult_hi]
+                else:
+                    raise ValidationError(
+                        f"Cannot process: please fully specify float multiplicity: m: {molecular_multiplicity} fm: {fragment_multiplicities}"
+                    )
+            for m in mult_range:
                 cgmp_exact_fm[ifr].append(m)
             cgmp_exact_fm[ifr].append(1)
             cgmp_exact_fm[ifr].append(2)


diff --git a/tests/dialects/test_tsql.py b/tests/dialects/test_tsql.py
index 738120a2ee..fe1aa9c5dd 100644
--- a/tests/dialects/test_tsql.py
+++ b/tests/dialects/test_tsql.py
@@ -436,6 +436,13 @@ def test_tsql(self):
             "'a' + 'b'",
         )

+        self.validate_identity(
+            "CREATE TABLE db.t1 (a INTEGER, b VARCHAR(50), CONSTRAINT c PRIMARY KEY (a DESC))",
+        )
+        self.validate_identity(
+            "CREATE TABLE db.t1 (a INTEGER, b INTEGER, CONSTRAINT c PRIMARY KEY (a DESC, b))"
+        )
+
     def test_option(self):
         possible_options = [
             "HASH GROUP",

TSQL: PRIMARY KEY constraint fails to parse if sort option is given

The following CREATE TABLE statement parses just fine:
```
import sqlglot

test = """CREATE TABLE dbo.test (
      id INT,
      name VARCHAR(50),
      CONSTRAINT pk_testTable PRIMARY KEY (id)
      ) ;"""

    parsed_script = sqlglot.parse(test, read="tsql")
```

However, when I add ASC or DESC to the PRIMARY KEY constraint, it fails to parse:

```
import sqlglot

test = """CREATE TABLE dbo.test (
    id INT,
    name VARCHAR(50),
    CONSTRAINT pk_testTable PRIMARY KEY (id DESC)
    ) ;"""

    parsed_script = sqlglot.parse(test, read="tsql")
```

Error Message:

```
Exception has occurred: ParseError
Expecting ). Line 4, Col: 45.
  bo.test (
        id INT,
        name VARCHAR(50),
        CONSTRAINT pk_testTable PRIMARY KEY (id DESC)
        ) ;
  File "C:\*snip*\main.py", line 96, in main
    parsed_script = sqlglot.parse(test, read="tsql")
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\*snip*\main.py", line 116, in <module>
    main()
sqlglot.errors.ParseError: Expecting ). Line 4, Col: 45.
  bo.test (
        id INT,
        name VARCHAR(50),
        CONSTRAINT pk_testTable PRIMARY KEY (id DESC)
        ) ;
```

Official Documentation: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql?view=sql-server-ver16#constraint

Edit: sqlglot version is 25.21.3.

pytest --json-report --json-report-file=report_pytest.json tests/dialects/test_tsql.py

diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 7f310b3b58..ae3f6cde66 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -5902,7 +5902,7 @@ def _parse_foreign_key(self) -> exp.ForeignKey:
         )

     def _parse_primary_key_part(self) -> t.Optional[exp.Expression]:
-        return self._parse_field()
+        return self._parse_ordered() or self._parse_field()

     def _parse_period_for_system_time(self) -> t.Optional[exp.PeriodForSystemTimeConstraint]:
         if not self._match(TokenType.TIMESTAMP_SNAPSHOT):


diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index 957eb9d5b9..c732688e55 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -372,6 +372,14 @@ def test_postgres(self):
             pretty=True,
         )

+        self.validate_all(
+            "SELECT CURRENT_TIMESTAMP + INTERVAL '-3 MONTH'",
+            read={
+                "mysql": "SELECT DATE_ADD(CURRENT_TIMESTAMP, INTERVAL -1 QUARTER)",
+                "postgres": "SELECT CURRENT_TIMESTAMP + INTERVAL '-3 MONTH'",
+                "tsql": "SELECT DATEADD(QUARTER, -1, GETDATE())",
+            },
+        )
         self.validate_all(
             "SELECT ARRAY[]::INT[] AS foo",
             write={

[BUG] Conversion from SQL Server's DATEADD(quarter...) to PostgreSQL not working properly

## Type of issue: Bug Report
Conversion from SQL Server's DATEADD(quarter...) to PostgreSQL not working properly

## Description
When trying to convert SQL on read dialect 'tsql' for SQL Server's DATEADD(quarter...) the conversion is not correct and does not fail

## Steps to reproduce
```python
import sqlglot

example_query = "SELECT category, SUM(revenue) as total_revenue FROM events WHERE event_date >= DATEADD(quarter, -1, GETDATE()) GROUP BY category;"
parsed_postgres = sqlglot.transpile(example_query , read="tsql", write="postgres")
print(parsed_postgres)
```

## Expected behaviour
This conversion should show something like:
```sql
SELECT category, SUM(revenue) AS total_revenue FROM events WHERE event_date >= CURRENT_TIMESTAMP + INTERVAL '-3 MONTHS' GROUP BY category
```

## Actual behavior
Right now it will show:
```sql
SELECT category, SUM(revenue) AS total_revenue FROM events WHERE event_date >= CURRENT_TIMESTAMP + INTERVAL '-1 QUARTER' GROUP BY category
```

## Notes
1. The current behavior will fail because, as shown in [postgres docs](https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT), QUARTER is not supported. However, the structure of the query is correct. Changing this "N QUARTER" to "3*N months" should work.
2. The number before "QUARTER" does not matter, that's why it should always be 3 times the number of quarter months.

Thanks for this amazing library I just discovered it and it's coming handy! 💯

pytest --json-report --json-report-file=report_pytest.json tests/dialects/test_postgres.py

diff --git a/sqlglot/dialects/postgres.py b/sqlglot/dialects/postgres.py
index 97589d5fbd..61a6dba438 100644
--- a/sqlglot/dialects/postgres.py
+++ b/sqlglot/dialects/postgres.py
@@ -742,3 +742,12 @@ def isascii_sql(self, expression: exp.IsAscii) -> str:
         @unsupported_args("this")
         def currentschema_sql(self, expression: exp.CurrentSchema) -> str:
             return "CURRENT_SCHEMA"
+
+        def interval_sql(self, expression: exp.Interval) -> str:
+            unit = expression.text("unit").lower()
+
+            if unit.startswith("quarter") and isinstance(expression.this, exp.Literal):
+                expression.this.replace(exp.Literal.number(int(expression.this.to_py()) * 3))
+                expression.args["unit"].replace(exp.var("MONTH"))
+
+            return super().interval_sql(expression)


diff --git a/tests/test_conversions.py b/tests/test_conversions.py
index 27951ee..6145411 100644
--- a/tests/test_conversions.py
+++ b/tests/test_conversions.py
@@ -305,6 +305,11 @@ def test_pre():
     assert md("
foo
\n
bar
\n
baz
", sub_symbol="^") == "\n\nfoo\n\n```\nbar\n```\n\nbaz"

+def test_q():
+    assert md('foo <q>quote</q> bar') == 'foo "quote" bar'
+    assert md('foo <q cite="https://example.com">quote</q> bar') == 'foo "quote" bar'
+
+
 def test_script():
     assert md('foo  bar') == 'foo  bar'

Inline Quotation element ignored

Html that looks like this:

```html
Here is <q>A Quote</q> from someone.
```

should be rendered as:

```
Here is “A Quote“ from someone.
```

https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/q

Solution:

```python
def convert_q(self, el, text, parent_tags):
    return '“' + text + '“'
```

pytest --json-report --json-report-file=report_pytest.json tests/test_conversions.py

diff --git a/markdownify/__init__.py b/markdownify/__init__.py
index 780761c..a43f3a7 100644
--- a/markdownify/__init__.py
+++ b/markdownify/__init__.py
@@ -650,6 +650,9 @@ def convert_pre(self, el, text, parent_tags):

         return '\n\n```%s\n%s\n```\n\n' % (code_language, text)

+    def convert_q(self, el, text, parent_tags):
+        return '"' + text + '"'
+
     def convert_script(self, el, text, parent_tags):
         return ''