Repository: ManimCommunity/manim

Issue:
Config option `quality` skips check on folder-wide `manim.cfg`
## Description of bug / unexpected behavior
Manim skips checking the `--quality` (or `-q`) flag on the config file `manim.cfg` folder-wide.

## Expected behavior
Passing `manim <file.py> SceneName` with `manim.cfg` should output the same as
`manim <file.py> SceneName -q<anything not high_quality>` on the terminal.


## How to reproduce the issue
Here's a sample content of `manim.cfg` and `SampleScene.py` saved in `SampleFolder`.

`manim.cfg`
```cfg
[CLI]
quality = medium_quality
flush_cache = True
preview = True
progress_bar = display
background_color = #d9d9d9
```

`SampleScene.py`
```python
from manim import *


class ShowAxes(Scene):
    def construct(self):
        grid_params = {'stroke_color':'#141414',
                       'stroke_opacity':0.5,
                       'stroke_width':1.75}
        axis_params = {'stroke_color':'#121212',
                       'stroke_width':5}
        x_range = [-8,8,1]
        y_range = [-8,8,1]
        
        grid = NumberPlane(x_range=x_range, y_range=y_range,
                           background_line_style=grid_params,
                           axis_config=axis_params,
                           y_axis_config=axis_params)
        
        self.play(Create(grid, run_time=3, lag_ratio=0))
        self.wait(2)
```

Naturally, Manim would read the config file to pass these CLI flags so I wouldn't type the same CLI flags on the terminal each time I render, that includes `-qm`. However, when run:

```
manim SampleScene.py ShowAxes
```

It outputs similar to `-qh`, which is the default quality.
https://github.com/ManimCommunity/manim/blob/2d98af0b2229e37cecb4a0604335d1e911c3ff9f/manim/constants.py#L266

I've also tried `quality = m` but still doesn't work

## Logs

<details><summary>Terminal Output</summary>

```
PS C:\Users\...\Documents\...\SampleFolder> manim SampleScene.py ShowAxes
Manim Community v0.11.0

[11/01/21 00:58:03] INFO     Animation 0 : Partial movie file written in 'C:\Users\...\Documents\...\SampleFolder\media\videos\SampleScene\1080p60\partial_movie_files\ShowAxes\874599486_2270581280_1577535401.mp4'      scene_file_writer.py:442                             
[11/01/21 00:58:05] INFO     Animation 1 : Partial movie file written in 'C:\Users\...\Documents\...\SampleFolder\media\videos\SampleScene\1080p60\partial_movie_files\ShowAxes\3320805013_1152718458_989967319.mp4'      scene_file_writer.py:442                             
                    INFO     File ready at 'C:\Users\...\Documents\...\SampleFolder\media\videos\SampleScene\1080p60\ShowAxes.mp4' scene_file_writer.py:627                             
                    INFO     Cache flushed. 2 file(s) deleted in C:\Users\...\Documents\...\SampleFolder\media\videos\SampleScene\1080p60\partial_movie_files\ShowAxes. scene_file_writer.py:619                             
                    INFO     Rendered ShowAxes                                                                                                                                                                            scene.py:231
                             Played 2 animations                           
                    INFO     Previewed File at: 'C:\Users\...\Documents\...\SampleFolder\media\videos\SampleScene\1080p60\ShowAxes.mp4'                                                                                   file_ops.py:193
```

</details>

<details><summary>Folder Directory</summary>

```
PS C:\Users\...\Documents\...\SampleFolder> tree /f
     
C:.
│  manim.cfg
│  SampleScene.py
│
├─media
│  ├─images
│  │  └─SampleScene
│  └─videos
│      └─SampleScene
│          └─1080p60
│              │  ShowAxes.mp4
│              │
│              └─partial_movie_files
│                  └─ShowAxes
│                          partial_movie_file_list.txt
│
└─__pycache__
        SampleScene.cpython-39.pyc
```

</details>


## System specifications

<details><summary>System Details</summary>

- OS: Windows 10 Pro v10.0.19043 Build 19043
- RAM: 16GB
- Python version: Python 3.9.6
- Installed modules:
```
Package             Version
------------------- ---------
backcall            0.2.0
certifi             2021.10.8
charset-normalizer  2.0.7
click               8.0.3
click-default-group 1.2.2
cloup               0.7.1
colorama            0.4.4
colour              0.1.5
commonmark          0.9.1
debugpy             1.4.1
decorator           5.1.0
glcontext           2.3.4
idna                3.3
ipykernel           6.1.0
ipython             7.26.0
ipython-genutils    0.2.0
isosurfaces         0.1.0
jedi                0.18.0
jupyter-client      6.1.12
jupyter-core        4.7.1
manim               0.11.0
ManimPango          0.3.1
mapbox-earcut       0.12.10
matplotlib-inline   0.1.2
moderngl            5.6.4
moderngl-window     2.4.0
multipledispatch    0.6.0
networkx            2.6.3
numpy               1.21.3
parso               0.8.2
pickleshare         0.7.5
Pillow              8.4.0
pip                 21.3.1
prompt-toolkit      3.0.19
pycairo             1.20.1
pydub               0.25.1
pyglet              1.5.21
Pygments            2.10.0
pyrr                0.10.3
python-dateutil     2.8.2
pywin32             301
pyzmq               22.2.1
requests            2.26.0
rich                10.12.0
scipy               1.7.1
screeninfo          0.6.7
setuptools          58.3.0
six                 1.16.0
skia-pathops        0.7.1
tornado             6.1
tqdm                4.62.3
traitlets           5.0.5
urllib3             1.26.7
watchdog            2.1.6
wcwidth             0.2.5
wheel               0.37.0
```
</details>

<details><summary>FFMPEG</summary>

```
ffmpeg version 4.4-essentials_build-www.gyan.dev Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 10.2.0 (Rev6, Built by MSYS2 project)
```
</details>

## Additional comments
Possibly related to #749 and it might be skipping some other flags too.

I've looked into the source code and I suspect around this function where it skips the render quality check. Though, I'm not well-versed in reading ManimCE source codes.
https://github.com/ManimCommunity/manim/blob/2d98af0b2229e37cecb4a0604335d1e911c3ff9f/manim/_config/utils.py#L119-L123



Discussion:

Resolving patch:
diff --git a/manim/_config/utils.py b/manim/_config/utils.py
--- a/manim/_config/utils.py
+++ b/manim/_config/utils.py
@@ -116,19 +116,12 @@ def make_config_parser(custom_file: str = None) -> configparser.ConfigParser:
     return parser
 
 
-def _determine_quality(args: argparse.Namespace) -> str:
-    for quality in constants.QUALITIES:
-        if quality == constants.DEFAULT_QUALITY:
-            # Skip so we prioritize anything that overwrites the default quality.
-            pass
-        elif getattr(args, quality, None) or (
-            hasattr(args, "quality")
-            and args.quality is not None
-            and args.quality == constants.QUALITIES[quality]["flag"]
-        ):
+def _determine_quality(qual: str) -> str:
+    for quality, values in constants.QUALITIES.items():
+        if values["flag"] is not None and values["flag"] == qual:
             return quality
 
-    return constants.DEFAULT_QUALITY
+    return qual
 
 
 class ManimConfig(MutableMapping):
@@ -273,6 +266,7 @@ class MyScene(Scene):
         "plugins",
         "preview",
         "progress_bar",
+        "quality",
         "save_as_gif",
         "save_sections",
         "save_last_frame",
@@ -635,6 +629,10 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig":
         if val:
             setattr(self, "media_width", val)
 
+        val = parser["CLI"].get("quality", fallback="", raw=True)
+        if val:
+            self.quality = _determine_quality(val)
+
         return self
 
     def digest_args(self, args: argparse.Namespace) -> "ManimConfig":
@@ -747,7 +745,7 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig":
                 )
 
         # Handle the quality flags
-        self.quality = _determine_quality(args)
+        self.quality = _determine_quality(getattr(args, "quality", None))
 
         # Handle the -r flag.
         rflag = args.resolution
@@ -1134,6 +1132,8 @@ def quality(self):
 
     @quality.setter
     def quality(self, qual: str) -> None:
+        if qual is None:
+            return
         if qual not in constants.QUALITIES:
             raise KeyError(f"quality must be one of {list(constants.QUALITIES.keys())}")
         q = constants.QUALITIES[qual]
@@ -1383,7 +1383,7 @@ def get_dir(self, key: str, **kwargs: str) -> Path:
             Traceback (most recent call last):
             KeyError: 'video_dir {media_dir}/videos/{module_name}/{quality} requires the following keyword arguments: module_name'
             >>> config.get_dir("video_dir", module_name="myfile").as_posix()
-            'my_media_dir/videos/myfile/1080p60.0'
+            'my_media_dir/videos/myfile/1080p60'
 
         Note the quality does not need to be passed as keyword argument since
         :class:`ManimConfig` does store information about quality.
@@ -1402,7 +1402,7 @@ def get_dir(self, key: str, **kwargs: str) -> Path:
             >>> config.get_dir(
             ...     "partial_movie_dir", module_name="myfile", scene_name="myscene"
             ... ).as_posix()
-            'my_media_dir/videos/myfile/1080p60.0/partial_movie_files/myscene'
+            'my_media_dir/videos/myfile/1080p60/partial_movie_files/myscene'
 
         Standard f-string syntax is used.  Arbitrary names can be used when
         defining directories, as long as the corresponding values are passed to
@@ -1445,7 +1445,7 @@ def get_dir(self, key: str, **kwargs: str) -> Path:
 
         all_args = {k: self._d[k] for k in dirs}
         all_args.update(kwargs)
-        all_args["quality"] = f"{self.pixel_height}p{self.frame_rate}"
+        all_args["quality"] = f"{self.pixel_height}p{self.frame_rate:g}"
 
         path = self._d[key]
         while "{" in path:
diff --git a/manim/cli/render/render_options.py b/manim/cli/render/render_options.py
--- a/manim/cli/render/render_options.py
+++ b/manim/cli/render/render_options.py
@@ -3,6 +3,8 @@
 import click
 from cloup import option, option_group
 
+from manim.constants import QUALITIES
+
 from ... import logger
 
 
@@ -59,15 +61,20 @@ def validate_resolution(ctx, param, value):
         "-q",
         "--quality",
         default=None,
-        type=click.Choice(["l", "m", "h", "p", "k"], case_sensitive=False),
-        help="""
-            Render quality at the follow resolution framerates, respectively:
-            854x480 30FPS,
-            1280x720 30FPS,
-            1920x1080 60FPS,
-            2560x1440 60FPS,
-            3840x2160 60FPS
-            """,
+        type=click.Choice(
+            reversed([q["flag"] for q in QUALITIES.values() if q["flag"]]),
+            case_sensitive=False,
+        ),
+        help="Render quality at the follow resolution framerates, respectively: "
+        + ", ".join(
+            reversed(
+                [
+                    f'{q["pixel_width"]}x{q["pixel_height"]} {q["frame_rate"]}FPS'
+                    for q in QUALITIES.values()
+                    if q["flag"]
+                ]
+            )
+        ),
     ),
     option(
         "-r",


Test patch: