Skip to content

Commit

Permalink
Merge pull request #2257 from yunline/power-state
Browse files Browse the repository at this point in the history
* add pygame.system.get_power_state

* Retuen None if the power state is unknown.

* add test for `get_power_state`

* update `get_power_state()`
return `_PowerState` instead of a dict

* Update tests for `get_power_state()`

* Use python dataclass

* Update docs

* _PowerState -> PowerState

* Update PowerState
- data_classes.py -> _data_classes.py
- set the __module__  of PowerState to "pygame.system"
- update stubs and tests
  • Loading branch information
yunline authored Sep 18, 2023
2 parents bf6f577 + 929400e commit 26b1af5
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 1 deletion.
5 changes: 4 additions & 1 deletion buildconfig/stubs/pygame/system.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import List, Optional
from typing import List, Optional, final

from typing_extensions import TypedDict

from pygame._data_classes import PowerState

class _InstructionSets(TypedDict):
RDTSC: bool
ALTIVEC: bool
Expand All @@ -28,3 +30,4 @@ def get_cpu_instruction_sets() -> _InstructionSets: ...
def get_total_ram() -> int: ...
def get_pref_path(org: str, app: str) -> str: ...
def get_pref_locales() -> List[_Locale]: ...
def get_power_state() -> Optional[PowerState]: ...
57 changes: 57 additions & 0 deletions docs/reST/ref/system.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,60 @@ open just in case something obvious comes up.
function again to get an updated copy of preferred locales.

.. versionadded:: 2.2.0

.. function:: get_power_state

| :sl:`get the current power supply state`
| :sg:`get_pref_power_state() -> PowerState`
Returns a ``PowerState`` object representing the power supply state.

Returns ``None`` if the power state is unknown.

The PowerState object has several attributes:

.. code-block:: text
battery_percent:
An integer, representing the seconds of battery life left.
Could be None if the value is unknown.
battery_seconds:
An integer between 0 and 100, representing the percentage of
battery life left.
on_battery:
True if the device is running on the battery (not plugged in).
no_battery:
True if the device has no battery available (plugged in).
charging:
True if the device is charging battery (plugged in).
charged:
True if the battery of the device is fully charged (plugged in).
plugged_in:
True if the device is plugged in.
Equivalent to `not on_battery`.
has_battery:
True if the device has battery.
Equivalent to `on_battery or not no_battery`.
You should never take a battery status as absolute truth. Batteries
(especially failing batteries) are delicate hardware, and the values
reported here are best estimates based on what that hardware reports. It's
not uncommon for older batteries to lose stored power much faster than it
reports, or completely drain when reporting it has 20 percent left, etc.

Battery status can change at any time; if you are concerned with power
state, you should call this function frequently, and perhaps ignore changes
until they seem to be stable for a few seconds.

It's possible a platform can only report battery percentage or time left
but not both.

.. versionadded:: 2.4.0
1 change: 1 addition & 0 deletions src_c/doc/system_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
#define DOC_SYSTEM_GETTOTALRAM "get_total_ram() -> ram_size\nget the amount of RAM configured in the system"
#define DOC_SYSTEM_GETPREFPATH "get_pref_path(org, app) -> path\nget a writeable folder for your app"
#define DOC_SYSTEM_GETPREFLOCALES "get_pref_locales() -> list[locale]\nget preferred locales set on the system"
#define DOC_SYSTEM_GETPOWERSTATE "get_pref_power_state() -> PowerState\nget the current power supply state"
89 changes: 89 additions & 0 deletions src_c/system.c
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,74 @@ pg_system_get_pref_locales(PyObject *self, PyObject *_null)
#endif
}

static PyObject *PowerState_class = NULL;

static PyObject *
pg_system_get_power_state(PyObject *self, PyObject *_null)
{
int sec, pct;
SDL_PowerState power_state;
PyObject *return_args;
PyObject *return_kwargs;
PyObject *sec_py, *pct_py;

power_state = SDL_GetPowerInfo(&sec, &pct);

if (power_state == SDL_POWERSTATE_UNKNOWN) {
Py_RETURN_NONE;
}

if (sec == -1) {
sec_py = Py_None;
Py_INCREF(Py_None);
}
else {
sec_py = PyLong_FromLong(sec);
}

if (pct == -1) {
pct_py = Py_None;
Py_INCREF(Py_None);
}
else {
pct_py = PyLong_FromLong(pct);
}
// Error check will be done in Py_BuildValue

int on_battery = (power_state == SDL_POWERSTATE_ON_BATTERY);
int no_battery = (power_state == SDL_POWERSTATE_NO_BATTERY);
int charging = (power_state == SDL_POWERSTATE_CHARGING);
int charged = (power_state == SDL_POWERSTATE_CHARGED);

// clang-format off
return_kwargs = Py_BuildValue(
"{s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N}",
"battery_percent", pct_py,
"battery_seconds", sec_py,
"on_battery", PyBool_FromLong(on_battery),
"no_battery", PyBool_FromLong(no_battery),
"charging", PyBool_FromLong(charging),
"charged", PyBool_FromLong(charged),
"plugged_in", PyBool_FromLong(!on_battery),
"has_battery", PyBool_FromLong(on_battery || !no_battery)
);
// clang-format on

if (!return_kwargs)
return NULL;

return_args = Py_BuildValue("()");

if (!return_args)
return NULL;

if (!PowerState_class) {
return RAISE(PyExc_SystemError, "PowerState class is not imported.");
}

return PyObject_Call(PowerState_class, return_args, return_kwargs);
}

static PyMethodDef _system_methods[] = {
{"get_cpu_instruction_sets", pg_system_get_cpu_instruction_sets,
METH_NOARGS, DOC_SYSTEM_GETCPUINSTRUCTIONSETS},
Expand All @@ -170,6 +238,8 @@ static PyMethodDef _system_methods[] = {
METH_VARARGS | METH_KEYWORDS, DOC_SYSTEM_GETPREFPATH},
{"get_pref_locales", pg_system_get_pref_locales, METH_NOARGS,
DOC_SYSTEM_GETPREFLOCALES},
{"get_power_state", pg_system_get_power_state, METH_NOARGS,
DOC_SYSTEM_GETPOWERSTATE},
{NULL, NULL, 0, NULL}};

MODINIT_DEFINE(system)
Expand All @@ -191,11 +261,30 @@ MODINIT_DEFINE(system)
return NULL;
}

PyObject *data_classes_module =
PyImport_ImportModule("pygame._data_classes");
if (!data_classes_module) {
return NULL;
}

PowerState_class =
PyObject_GetAttrString(data_classes_module, "PowerState");
if (!PowerState_class) {
return NULL;
}
Py_DECREF(data_classes_module);

/* create the module */
module = PyModule_Create(&_module);
if (!module) {
return NULL;
}

if (PyModule_AddObject(module, "PowerState", PowerState_class)) {
Py_DECREF(PowerState_class);
Py_DECREF(module);
return NULL;
}

return module;
}
4 changes: 4 additions & 0 deletions src_py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ def PixelArray(surface): # pylint: disable=unused-argument

try:
import pygame.system
from pygame._data_classes import PowerState as power_state

power_state.__module__ = "pygame.system"
del power_state
except (ImportError, OSError):
system = MissingModule("system", urgent=0)

Expand Down
14 changes: 14 additions & 0 deletions src_py/_data_classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class PowerState:
battery_percent: Optional[int]
battery_seconds: Optional[int]
on_battery: bool
no_battery: bool
charging: bool
charged: bool
plugged_in: bool
has_battery: bool
41 changes: 41 additions & 0 deletions test/system_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,47 @@ def test_get_pref_locales(self):
for arg in (None, 1, "hello"):
self.assertRaises(TypeError, pygame.system.get_pref_locales, arg)

def test_get_power_state(self):
power_state = pygame.system.get_power_state()
self.assertIsInstance(power_state, (type(None), pygame.system.PowerState))

expected_types = {
"battery_seconds": (type(None), int),
"battery_percent": (type(None), int),
"on_battery": bool,
"no_battery": bool,
"charging": bool,
"charged": bool,
"plugged_in": bool,
"has_battery": bool,
}

if power_state is not None:
for attr_name in expected_types:
self.assertTrue(hasattr(power_state, attr_name))
self.assertIsInstance(
getattr(power_state, attr_name), expected_types[attr_name]
)

self.assertTrue(power_state.plugged_in == (not power_state.on_battery))
self.assertTrue(
power_state.has_battery
== (power_state.on_battery or not power_state.no_battery)
)

# There should be only one `True`
self.assertEqual(
sum(
[
power_state.on_battery,
power_state.no_battery,
power_state.charged,
power_state.charging,
]
),
1,
)


if __name__ == "__main__":
unittest.main()

0 comments on commit 26b1af5

Please sign in to comment.