Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can I use aiofile.async_open without a with statement? #67

Closed
allComputableThings opened this issue May 6, 2022 · 4 comments · Fixed by #71
Closed

Can I use aiofile.async_open without a with statement? #67

allComputableThings opened this issue May 6, 2022 · 4 comments · Fixed by #71

Comments

@allComputableThings
Copy link

Python's open may be used directly, or as a context manager in a with block.
I love contextmanagers, but they're not always the right tool because cleanup on the stack is not always the right time to perform cleanup.
Can I use aiofile.async_open without a with statement?

import asyncio

import aiofile

async def test():
    f = aiofile.async_open('filename.txt', mode='w')
    try:
        await f.write('123') #  asyncio.base_futures.InvalidStateError: AIOFile closed
    finally:
        pass
        # await f.close()
asyncio.run(test())
@thomas-mckay
Copy link

thomas-mckay commented Aug 5, 2022

You could write:

    async def test():
        f = aiofile.async_open('filename.txt', mode='w')
        try:
            await f.file
            await f.write('123')
        finally:
            pass
            # await f.close()

    asyncio.run(test())

But I don't really understand the need for this from your example. The only thing I can think of would be that maybe you'd want to return f to whatever called test so it can do more writes before closing the file but then, why not something like this instead:

    @asynccontextmanager
    async def foo():
        async with aiofile.async_open('filename.txt', mode='w') as afp:
            await afp.write('123')
            yield afp

    async def bar():
        async with foo() as afp:
            await afp.write('456')

    asyncio.run(bar())

This seems safer to ensure afp is eventually closed.

@allComputableThings
Copy link
Author

You could write:

Thank you -- is helpful.

This seems safer to ensure afp is eventually closed.

Sure - but many applications are not so trivial and the thing writing to the file may not have async_open in its stack.
For instance:

  def logMessage(self, msg)
      async with self.writeLock:
          ...
          await self.aiofile.write(msg)
          ...
      
  def onRecvX(self, msg):  # Runs concurrently as a task
        ... 
        await self.logMessage(msg)
        
   def onRecvY(self, msg):  # Runs concurrently as a task
        ... 
        await self.logMessage(msg)

   def onRecvZ(self, msg):  # Runs concurrently as a task
        ... 
        await self.logMessage(msg)

Simply, the write call might not be in the stack of onRecv.... The file may be opened lazily in logMessage. The file might also be closed - it may be log-rotated, for instance. I don't want to unwind the stack and cancel all running asyncio workers that have started since the file was opened just so that I can open/reopen the file.

@thomas-mckay
Copy link

thomas-mckay commented Aug 7, 2022

Thank you, I understand your requirements a little better. Here's how I'd do it with the log rotate example:

import asyncio
from datetime import datetime
from pathlib import Path

from aiofile import AIOFile, Writer


class AsyncRotatedLog:
    def __init__(self, directory='./logs/'):
        self._aio_file = None
        self._current_log_path = None
        self._writer = None
        self._directory = Path(directory).resolve()
        self._lock = asyncio.Lock()

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self._aio_file:
            await self._aio_file.close()

    async def _get_writer(self):
        async with self._lock:
            filename = self._directory / f'{datetime.now().strftime("%Y.%m.%d_%H:%M:%S")}.log'
            if filename != self._current_log_path:
                if self._aio_file:
                    await self._aio_file.close()
                self._aio_file = await AIOFile(filename, 'w')
                self._current_log_path = filename
                self._writer = Writer(self._aio_file)
            return self._writer

    async def write(self, msg: str):
        msg = msg.strip('\n') + '\n'
        writer = await self._get_writer()
        await writer(msg)


class Foo:
    def __init__(self, async_logger):
        self.async_logger = async_logger

    async def log_message(self, msg):
        await self.async_logger.write(msg)

    async def on_x(self, msg):  # Runs concurrently as a task
        ...
        await self.log_message(msg)

    async def on_y(self, msg):  # Runs concurrently as a task
        ...
        await self.log_message(msg)

    async def on_z(self, msg):  # Runs concurrently as a task
        ...
        await self.log_message(msg)

    async def listen_for_xyz(self):
        await self.on_x('X happened')
        await asyncio.sleep(0.2)
        await self.on_y('Y happened')
        await asyncio.sleep(1.5)
        await self.on_z('Z happened')


async def main():
    async with AsyncRotatedLog() as logger:
        foo = Foo(logger)
        await foo.listen_for_xyz()


asyncio.run(main())

Basically, encapsulate your log rotation logic in its own class and make it an async context manager (this part is optional, you could just as easily replace the __aexit__(self) with a close(self) method and wrap your calling code in a try... finally. The important thing here is that you ensure your file descriptor are always closed on exit.

Disclaimer: this is simple example code. It's working but it may have bugs (I didn't spend much time on it). Use at your own risks.

@mosquito
Copy link
Owner

mosquito commented Aug 8, 2022

@stuz5000 this will be fixed in version 3.8.0.

Code example:

import asyncio
import atexit
import os
from tempfile import mktemp

from aiofile import async_open


TMP_NAME = mktemp()
atexit.register(os.unlink, TMP_NAME)


async def main():
    afp = await async_open(TMP_NAME, "w")
    await afp.write("Hello")
    await afp.close()


asyncio.run(main())
assert open(TMP_NAME, "r").read() == "Hello"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants